<commit_before>var expect        = require('expect.js');
var _             = require('lodash');
var sampleData    = require('./lib/sampleData');
var fruits        = sampleData.fruits;
var fns           = sampleData.fns;
var helpers       = require('./lib/helpers');
var setupModel    = helpers.getModelSetup(fns, '*', ['name', 'color']);

describe('Model.view', function () {
  describe('Setup', function () {
    it('Returns name of created view with empty collection', function () {
      var model = setupModel();
      var view  = model.at('fruits').view('yellowFruitsWithPath');
      expect(view.viewName).to.equal('yellowFruitsWithPath');
    });

    it('Returns name of created view with non-empty collection', function () {
      var model = setupModel({fruits: fruits});
      var view =  model.at('fruits').view('yellowFruitsWithPath');
      expect(view.viewName).to.equal('yellowFruitsWithPath');
    });
  });

  describe('Referencing', function () {
    it('Returns empty on empty collection', function () {
      var model = setupModel();
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      expect(model.get('filteredFruits')).to.be.empty();
    });

    it('Returns filtered data on non-empty collection', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Adding items to empty collection', function () {
    it('Returns empty with filtered item', function () {
      var model = setupModel();
      var view  = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.add('fruits', {name: 'grapefruit', color: 'orange', amount: 10, id: 'grapefruitId'});
      expect(model.get('filteredFruits')).to.be.empty();
    });

    it('Returns new item with non-filtered item', function () {
      var model = setupModel();
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.add('fruits', {name: 'banana', color: 'yellow', amount: 15, id: 'bananaId'});
      var expectedFruits = model.expectedResult({fruits: ['bananaId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Adding items to non-empty collection', function () {
    it('Returns original data with filtered item', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.add('fruits', {name: 'grapefruit', color: 'orange', amount: 10, id: 'grapefruitId'});
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it('Returns updated view with non-filtered item', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.add('fruits', {name: 'mango', color: 'yellow', amount: 15, id: 'mangoId'});
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId', 'mangoId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Removing item from collection', function () {
    it('Returns unchanged when item is not emitted', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.del('fruits.appleId');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it('Returns updated view when item was previously emitted', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.del('fruits' + '.bananaId');
      var expectedFruits = model.expectedResult({fruits: ['lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Updating item in collection', function () {
    it('Returns new item when change previously did not cause emit but now does', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.set('fruits.appleId.color', 'yellow');
      var expectedFruits = model.expectedResult({fruits: ['appleId', 'bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it('Returns without item when change previosuly caused emit but no longer does', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.set('fruits' + '.bananaId.color', 'green');
      var expectedFruits = model.expectedResult({fruits: ['lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it("Returns un-changed results when change did not affect the emitted key/path", function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.set('fruits' + '.bananaId.color', 'yellow');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it('Returns changed when change changed key and path was and is still emitted', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.set('fruits' + '.bananaId.name', 'weird-banana');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    describe('Without ref with init', function () {
      it('Returns changed when change changed key and path was and is still emitted', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsWithPath');
        view._init();
        model.set('fruits' + '.bananaId.name', 'weird-banana');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
        expect(model._get(view.pathsSegments)).to.eql(expectedFruits);
      });
    });
  });

  describe('Passing in functions NOT defined with model.fn()', function () {
    it('Returns empty on empty collection', function () {
      var model = setupModel();
      var view = model.at('fruits').view(fns['yellowFruitsWithPath']);
      view.ref('_page.filteredFruits');
      expect(model.get('filteredFruits')).to.be.empty();
    });

    it('Returns data with non-empty collection', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view(fns['yellowFruitsWithPath']);
      view.ref('_page.filteredFruits');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Passing in only "key" argument to emit()', function () {
    describe('Function declared with model.fn()', function () {
      it('Returns empty on empty collection', function () {
        var model = setupModel();
        var view  = model.at('fruits').view('yellowFruits');
        view.ref('_page.filteredFruits');
        expect(model.get('filteredFruits')).to.be.empty();
      });

      it('Returns data with non-empty collection', function () {
        var model = setupModel({fruits: fruits});
        var view =  model.at('fruits').view('yellowFruits');
        view.ref('_page.filteredFruits');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Function NOT defined with model.fn()', function () {
      it('Returns empty on empty collection', function () {
        var model = setupModel();
        var view = model.at('fruits').view(fns['yellowFruits']);
        view.ref('_page.filteredFruits');
        expect(model.get('filteredFruits')).to.be.empty();
      });

      it('Returns data with non-empty collection', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view(fns['yellowFruits']);
        view.ref('_page.filteredFruits');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });
  });

  describe('Multilevel keys', function () {
    describe('Basic functionality', function () {
      it('Returns data properly structured', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });

      it('Works with only "key" argument into emit()', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Adding item to collection', function () {
      it('View remains unchanged when adding filtered item', function () {
        var model = setupModel();
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.add('fruits', {name: 'grapefruit', color: 'orange', amount: 10, id: 'grapefruitId'});
        expect(model.get('filteredFruits')).to.be.empty();
      });

      it('Adds item when adding non-filtered item', function () {
        var model = setupModel({fruits: fruits.slice(0, 3)});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.add('fruits', {name: 'lemon', color: 'yellow', amount: 10, id: 'lemonId'});
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Removing item from collection', function () {
      it('View remains unchanged when removing filtered item', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.del('fruits.appleId');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });

      it('Removes item when removing non-filtered item', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.del('fruits.bananaId');
        var expectedFruits = model.expectedResult({fruits: ['lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Updating item in collection', function () {
      it('Adds item when it was previously filtered, but no longer is', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.set('fruits.appleId.color', 'yellow');
        var expectedFruits = model.expectedResult({fruits: ['appleId', 'bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });

      it('Removes item when it was previously not filtered but now is', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.set('fruits' + '.bananaId.color', 'green');
        var expectedFruits = model.expectedResult({fruits: ['lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });
  });

  describe('Other path than self', function () {
    describe('Basic functionality', function () {
      it('Returns filtered keys with corresponding items', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsRelated');
        view.ref('_page.filteredFruits');
        // model.set('fruits' + '.bananaId.related', 'yellow');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {related: true});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Updating item in collection', function () {
      it('Changes path when changing path dependent property', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsRelated');
        view.ref('_page.filteredFruits');
        model.set('fruits' + '.bananaId.related', 'orangeId');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {related: true});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    })
  });
});
<commit_msg>Add test checking if one view per path+viewName combo is only allowed<commit_after>var expect        = require('expect.js');
var _             = require('lodash');
var sampleData    = require('./lib/sampleData');
var fruits        = sampleData.fruits;
var fns           = sampleData.fns;
var helpers       = require('./lib/helpers');
var setupModel    = helpers.getModelSetup(fns, '*', ['name', 'color']);

describe('Model.view', function () {
  describe('Setup', function () {
    it('Returns name of created view with empty collection', function () {
      var model = setupModel();
      var view  = model.at('fruits').view('yellowFruitsWithPath');
      expect(view.viewName).to.equal('yellowFruitsWithPath');
    });

    it('Returns name of created view with non-empty collection', function () {
      var model = setupModel({fruits: fruits});
      var view =  model.at('fruits').view('yellowFruitsWithPath');
      expect(view.viewName).to.equal('yellowFruitsWithPath');
    });

    it('Returns the same view if the view is created twice', function () {
      var model = setupModel({fruits: fruits});
      var view1 =  model.at('fruits').view('yellowFruitsWithPath');
      var view2 =  model.at('fruits').view('yellowFruitsWithPath');
      expect(view1).to.equal(view2);
    });
  });

  describe('Referencing', function () {
    it('Returns empty on empty collection', function () {
      var model = setupModel();
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      expect(model.get('filteredFruits')).to.be.empty();
    });

    it('Returns filtered data on non-empty collection', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Adding items to empty collection', function () {
    it('Returns empty with filtered item', function () {
      var model = setupModel();
      var view  = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.add('fruits', {name: 'grapefruit', color: 'orange', amount: 10, id: 'grapefruitId'});
      expect(model.get('filteredFruits')).to.be.empty();
    });

    it('Returns new item with non-filtered item', function () {
      var model = setupModel();
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.add('fruits', {name: 'banana', color: 'yellow', amount: 15, id: 'bananaId'});
      var expectedFruits = model.expectedResult({fruits: ['bananaId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Adding items to non-empty collection', function () {
    it('Returns original data with filtered item', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.add('fruits', {name: 'grapefruit', color: 'orange', amount: 10, id: 'grapefruitId'});
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it('Returns updated view with non-filtered item', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.add('fruits', {name: 'mango', color: 'yellow', amount: 15, id: 'mangoId'});
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId', 'mangoId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Removing item from collection', function () {
    it('Returns unchanged when item is not emitted', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.del('fruits.appleId');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it('Returns updated view when item was previously emitted', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.del('fruits' + '.bananaId');
      var expectedFruits = model.expectedResult({fruits: ['lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Updating item in collection', function () {
    it('Returns new item when change previously did not cause emit but now does', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.set('fruits.appleId.color', 'yellow');
      var expectedFruits = model.expectedResult({fruits: ['appleId', 'bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it('Returns without item when change previosuly caused emit but no longer does', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.set('fruits' + '.bananaId.color', 'green');
      var expectedFruits = model.expectedResult({fruits: ['lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it("Returns un-changed results when change did not affect the emitted key/path", function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.set('fruits' + '.bananaId.color', 'yellow');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    it('Returns changed when change changed key and path was and is still emitted', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view('yellowFruitsWithPath');
      view.ref('_page.filteredFruits');
      model.set('fruits' + '.bananaId.name', 'weird-banana');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });

    describe('Without ref with init', function () {
      it('Returns changed when change changed key and path was and is still emitted', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsWithPath');
        view._init();
        model.set('fruits' + '.bananaId.name', 'weird-banana');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
        expect(model._get(view.pathsSegments)).to.eql(expectedFruits);
      });
    });
  });

  describe('Passing in functions NOT defined with model.fn()', function () {
    it('Returns empty on empty collection', function () {
      var model = setupModel();
      var view = model.at('fruits').view(fns['yellowFruitsWithPath']);
      view.ref('_page.filteredFruits');
      expect(model.get('filteredFruits')).to.be.empty();
    });

    it('Returns data with non-empty collection', function () {
      var model = setupModel({fruits: fruits});
      var view = model.at('fruits').view(fns['yellowFruitsWithPath']);
      view.ref('_page.filteredFruits');
      var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
      expect(model.get('filteredFruits')).to.eql(expectedFruits);
    });
  });

  describe('Passing in only "key" argument to emit()', function () {
    describe('Function declared with model.fn()', function () {
      it('Returns empty on empty collection', function () {
        var model = setupModel();
        var view  = model.at('fruits').view('yellowFruits');
        view.ref('_page.filteredFruits');
        expect(model.get('filteredFruits')).to.be.empty();
      });

      it('Returns data with non-empty collection', function () {
        var model = setupModel({fruits: fruits});
        var view =  model.at('fruits').view('yellowFruits');
        view.ref('_page.filteredFruits');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Function NOT defined with model.fn()', function () {
      it('Returns empty on empty collection', function () {
        var model = setupModel();
        var view = model.at('fruits').view(fns['yellowFruits']);
        view.ref('_page.filteredFruits');
        expect(model.get('filteredFruits')).to.be.empty();
      });

      it('Returns data with non-empty collection', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view(fns['yellowFruits']);
        view.ref('_page.filteredFruits');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });
  });

  describe('Multilevel keys', function () {
    describe('Basic functionality', function () {
      it('Returns data properly structured', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });

      it('Works with only "key" argument into emit()', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Adding item to collection', function () {
      it('View remains unchanged when adding filtered item', function () {
        var model = setupModel();
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.add('fruits', {name: 'grapefruit', color: 'orange', amount: 10, id: 'grapefruitId'});
        expect(model.get('filteredFruits')).to.be.empty();
      });

      it('Adds item when adding non-filtered item', function () {
        var model = setupModel({fruits: fruits.slice(0, 3)});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.add('fruits', {name: 'lemon', color: 'yellow', amount: 10, id: 'lemonId'});
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Removing item from collection', function () {
      it('View remains unchanged when removing filtered item', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.del('fruits.appleId');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });

      it('Removes item when removing non-filtered item', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.del('fruits.bananaId');
        var expectedFruits = model.expectedResult({fruits: ['lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Updating item in collection', function () {
      it('Adds item when it was previously filtered, but no longer is', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.set('fruits.appleId.color', 'yellow');
        var expectedFruits = model.expectedResult({fruits: ['appleId', 'bananaId', 'lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });

      it('Removes item when it was previously not filtered but now is', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsMultilevelWithPath');
        view.ref('_page.filteredFruits');
        model.set('fruits' + '.bananaId.color', 'green');
        var expectedFruits = model.expectedResult({fruits: ['lemonId']}, {separator: '.'});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });
  });

  describe('Other path than self', function () {
    describe('Basic functionality', function () {
      it('Returns filtered keys with corresponding items', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsRelated');
        view.ref('_page.filteredFruits');
        // model.set('fruits' + '.bananaId.related', 'yellow');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {related: true});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    });

    describe('Updating item in collection', function () {
      it('Changes path when changing path dependent property', function () {
        var model = setupModel({fruits: fruits});
        var view = model.at('fruits').view('yellowFruitsRelated');
        view.ref('_page.filteredFruits');
        model.set('fruits' + '.bananaId.related', 'orangeId');
        var expectedFruits = model.expectedResult({fruits: ['bananaId', 'lemonId']}, {related: true});
        expect(model.get('filteredFruits')).to.eql(expectedFruits);
      });
    })
  });
});
<|endoftext|>


<commit_before><commit_msg>fix(create-hops-app): exit when unpacking tar archive errors<commit_after><|endoftext|>


<commit_before>import Index from '../utility/index.js';
import Match from '../utility/match.js';
import Includes from '../utility/includes.js';
import Multiple from '../utility/multiple.js';

export default function (binder, caller) {
    const self = this;
    const type = binder.target.type;
    let data;

    if (binder.meta.busy) return;
    else binder.meta.busy = true;

    if (!binder.meta.setup) {
        binder.meta.setup = true;
        binder.target.addEventListener('input', () => {
            this.render(binder, 'view');
            // binder.data = binder.target.value;
        }, false);
    }

    if (type === 'select-one' || type === 'select-multiple') {
        return {
            read() {

                this.data = binder.data;
                this.model = binder.model;
                this.options = binder.target.options;
                this.multiple = Multiple(binder.target);

                if (this.multiple && (!this.data || this.data.constructor !== Array)) {
                    binder.meta.busy = false;
                    throw new Error(`Oxe - invalid o-value ${binder.keys.join('.')} multiple select requires array`);
                }

            },
            write() {
                let fallback = false;
                let fallbackSelectedAtrribute = false;
                let fallbackValue = this.multiple ? [] : null;
                let fallbackOption = this.multiple ? [] : null;

                for (let i = 0, l = this.options.length; i < l; i++) {

                    const option = this.options[i];
                    const selected = option.selected;
                    const optionBinder = self.get(option, 'o-value');
                    const optionValue = optionBinder ? optionBinder.data : option.value;
                    const selectedAtrribute = option.hasAttribute('selected');

                    if (this.multiple) {
                        if (selectedAtrribute) {
                            fallback = true;
                            fallbackOption.push(option);
                            fallbackValue.push(optionValue);
                        }
                    } else {
                        if (i === 0 || selectedAtrribute) {
                            fallback = true;
                            fallbackOption = option;
                            fallbackValue = optionValue;
                            fallbackSelectedAtrribute = selectedAtrribute;
                        }
                    }

                    if (caller === 'view') {
                        if (selected) {
                            if (this.multiple) {
                                const includes = Includes(this.data, optionValue);
                                if (!includes) {
                                    this.selected = true;
                                    binder.data.push(optionValue);
                                }
                            } else if (!this.selected) {
                                this.selected = true;
                                binder.data = optionValue;
                            }
                        } else {
                            if (this.multiple) {
                                const index = Index(this.data, optionValue);
                                if (index !== -1) {
                                    binder.data.splice(index, 1);
                                }
                            } else if (!this.selected && i === l - 1) {
                                binder.data = null;
                            }
                        }
                    } else {
                        if (this.multiple) {
                            const includes = Includes(this.data, optionValue);
                            if (includes) {
                                this.selected = true;
                                option.selected = true;
                            } else {
                                option.selected = false;
                            }
                        } else {
                            if (!this.selected) {
                                const match = Match(this.data, optionValue);
                                if (match) {
                                    this.selected = true;
                                    option.selected = true;
                                } else {
                                    option.selected = false;
                                }
                            } else {
                                option.selected = false;
                            }
                        }
                    }

                }

                if (!this.selected && fallback) {
                    if (this.multiple) {
                        for (let i = 0, l = fallbackOption.length; i < l; i++) {
                            fallbackOption[i].selected = true;
                            binder.data.push(fallbackValue[i]);
                        }
                    } else if (fallbackSelectedAtrribute || this.nodeName === 'OPTION') {
                        binder.data = fallbackValue;
                        fallbackOption.selected = true;
                    }
                }

                binder.meta.busy = false;
            }
        };
    } else if (type === 'radio') {
        return {
            read() {

                this.form = binder.target.form || binder.container;
                this.query = `input[type="radio"][o-value="${binder.value}"]`;
                this.nodes = this.form.querySelectorAll(this.query);
                this.radios = Array.prototype.slice.call(this.nodes);

                if (caller === 'view') {
                    binder.data = this.radios.indexOf(binder.target);
                    binder.meta.busy = false;
                    return this.write = false;
                }

                if (typeof binder.data !== 'number') {
                    binder.meta.busy = false;
                    return this.write = false;
                }

            },
            write() {
                for (let i = 0, l = this.radios.length; i < l; i++) {
                    const radio = this.radios[i];

                    if (i === binder.data) {
                        radio.checked = true;
                    } else {
                        radio.checked = false;
                    }

                }

                binder.meta.busy = false;
            }
        };

    } else if (type === 'checkbox') {
        return {
            read() {

                if (caller === 'view') {
                    binder.data = binder.target.checked;
                    binder.meta.busy = false;
                    return this.write = false;
                }

                if (typeof binder.data !== 'boolean') {
                    binder.meta.busy = false;
                    return this.write = false;
                }

            },
            write() {
                binder.target.checked = binder.data;
                binder.meta.busy = false;
            }
        };
    } else if (type === 'file') {
        return {
            read() {
                this.multiple = Multiple(binder.target);
                binder.data = this.multiple ? Array.prototype.slice.call(binder.target.files) : binder.target.files[0];
                binder.meta.busy = false;
            }
        };
    } else {
        return {
            read() {
                data = binder.data;

                if (data === binder.target.value) {
                    binder.meta.busy = false;
                    return this.write = false;
                }

                if (caller === 'view') {
                    binder.data = binder.target.value;
                    binder.meta.busy = false;
                    return this.write = false;
                }

            },
            write() {
                binder.target.value = data === undefined || data === null ? '' : data;
                binder.meta.busy = false;
            }
        };
    }
}
<commit_msg>removed old variables<commit_after>import Index from '../utility/index.js';
import Match from '../utility/match.js';
import Includes from '../utility/includes.js';
import Multiple from '../utility/multiple.js';

export default function (binder, caller) {
    const self = this;
    const type = binder.target.type;

    if (binder.meta.busy) return;
    else binder.meta.busy = true;

    if (!binder.meta.setup) {
        binder.meta.setup = true;
        binder.target.addEventListener('input', () => {
            this.render(binder, 'view');
            // binder.data = binder.target.value;
        }, false);
    }

    if (type === 'select-one' || type === 'select-multiple') {
        return {
            read() {

                this.data = binder.data;
                this.options = binder.target.options;
                this.multiple = Multiple(binder.target);

                if (this.multiple && (!this.data || this.data instanceof Array === false)) {
                    binder.meta.busy = false;
                    throw new Error(`Oxe - invalid o-value ${binder.keys.join('.')} multiple select requires array`);
                }

            },
            write() {
                let fallback = false;
                let fallbackSelectedAtrribute = false;
                let fallbackValue = this.multiple ? [] : null;
                let fallbackOption = this.multiple ? [] : null;

                for (let i = 0, l = this.options.length; i < l; i++) {

                    const option = this.options[i];
                    const selected = option.selected;
                    const optionBinder = self.get(option, 'o-value');
                    const optionValue = optionBinder ? optionBinder.data : option.value;
                    const selectedAtrribute = option.hasAttribute('selected');

                    if (this.multiple) {
                        if (selectedAtrribute) {
                            fallback = true;
                            fallbackOption.push(option);
                            fallbackValue.push(optionValue);
                        }
                    } else {
                        if (i === 0 || selectedAtrribute) {
                            fallback = true;
                            fallbackOption = option;
                            fallbackValue = optionValue;
                            fallbackSelectedAtrribute = selectedAtrribute;
                        }
                    }

                    if (caller === 'view') {
                        if (selected) {
                            if (this.multiple) {
                                const includes = Includes(this.data, optionValue);
                                if (!includes) {
                                    this.selected = true;
                                    binder.data.push(optionValue);
                                }
                            } else if (!this.selected) {
                                this.selected = true;
                                binder.data = optionValue;
                            }
                        } else {
                            if (this.multiple) {
                                const index = Index(this.data, optionValue);
                                if (index !== -1) {
                                    binder.data.splice(index, 1);
                                }
                            } else if (!this.selected && i === l - 1) {
                                binder.data = null;
                            }
                        }
                    } else {
                        if (this.multiple) {
                            const includes = Includes(this.data, optionValue);
                            if (includes) {
                                this.selected = true;
                                option.selected = true;
                            } else {
                                option.selected = false;
                            }
                        } else {
                            if (!this.selected) {
                                const match = Match(this.data, optionValue);
                                if (match) {
                                    this.selected = true;
                                    option.selected = true;
                                } else {
                                    option.selected = false;
                                }
                            } else {
                                option.selected = false;
                            }
                        }
                    }

                }

                if (!this.selected && fallback) {
                    if (this.multiple) {
                        for (let i = 0, l = fallbackOption.length; i < l; i++) {
                            fallbackOption[i].selected = true;
                            binder.data.push(fallbackValue[i]);
                        }
                    } else if (fallbackSelectedAtrribute || this.nodeName === 'OPTION') {
                        binder.data = fallbackValue;
                        fallbackOption.selected = true;
                    }
                }

                binder.meta.busy = false;
            }
        };
    } else if (type === 'radio') {
        return {
            read() {

                this.form = binder.target.form || binder.container;
                this.query = `input[type="radio"][o-value="${binder.value}"]`;
                this.nodes = this.form.querySelectorAll(this.query);
                this.radios = Array.prototype.slice.call(this.nodes);

                if (caller === 'view') {
                    binder.data = this.radios.indexOf(binder.target);
                    binder.meta.busy = false;
                    return this.write = false;
                }

                if (typeof binder.data !== 'number') {
                    binder.meta.busy = false;
                    return this.write = false;
                }

            },
            write() {
                for (let i = 0, l = this.radios.length; i < l; i++) {
                    const radio = this.radios[i];

                    if (i === binder.data) {
                        radio.checked = true;
                    } else {
                        radio.checked = false;
                    }

                }

                binder.meta.busy = false;
            }
        };

    } else if (type === 'checkbox') {
        return {
            read() {

                if (caller === 'view') {
                    binder.data = binder.target.checked;
                    binder.meta.busy = false;
                    return this.write = false;
                }

                if (typeof binder.data !== 'boolean') {
                    binder.meta.busy = false;
                    return this.write = false;
                }

            },
            write() {
                binder.target.checked = binder.data;
                binder.meta.busy = false;
            }
        };
    } else if (type === 'file') {
        return {
            read() {
                this.multiple = Multiple(binder.target);
                binder.data = this.multiple ? Array.prototype.slice.call(binder.target.files) : binder.target.files[0];
                binder.meta.busy = false;
            }
        };
    } else {
        return {
            read() {
                this.data = binder.data;

                if (data === binder.target.value) {
                    binder.meta.busy = false;
                    return this.write = false;
                }

                if (caller === 'view') {
                    binder.data = binder.target.value;
                    binder.meta.busy = false;
                    return this.write = false;
                }

            },
            write() {
                binder.target.value = this.data === undefined || this.data === null ? '' : this.data;
                binder.meta.busy = false;
            }
        };
    }
}
<|endoftext|>


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

// Node for Node-Red that lists all files from a given start folder
// with an optionally specified file extension.
//var fs = require('fs')
var path = require('path')
var readdirp = require('readdirp')

// Module name must match this nodes html file
var moduleName = 'fs-file-lister'

module.exports = function(RED) {
    'use strict'

    // The main node definition - most things happen in here
    function nodeGo(config) {
        // Create a RED node
        RED.nodes.createNode(this, config)

        // copy "this" object in case we need it in context of callbacks of other functions.
        var node = this

        // Store local copies of the node configuration (as defined in the .html)
        node.topic    = config.topic
        node.name     = config.name
        node.start    = config.start
        node.pattern  = config.pattern
        node.folders  = config.folders !== undefined ? config.folders : "*"
        node.lstype   = config.lstype  !== undefined ? config.lstype : "files"
        node.hidden   = config.hidden  !== undefined ? config.hidden : true  /** @since v1.0.2 */
        node.path     = config.path
        node.single   = config.single
        node.depth    = config.depth
        node.stat     = config.stat

		if ( (node.pattern == "") || (node.pattern == "*") ) node.pattern = "*.*"
		if  (node.folders == "") node.folders = "*"

        // Make sure the parameters are strings
        if ( (typeof node.start !== 'string') || (typeof node.pattern !== 'string') ) {
            node.error('Either Folder or Ext is not a string, cannot process. Folder: ' + node.start + ', Pattern: ' + node.ext)
            return
        }

        // respond to inputs....
        /** @since v1.0.1, amended ready for Node-RED v1 */
        node.on('input', function(msg, send, done) {

          // If this is pre-1.0, 'send' will be undefined, so fallback to node.send
            send = send || function() { node.send.apply(node,arguments) }
            // If this is pre-1.0, 'done' will be undefined, so fallback to dummy function
            done = done || function() { if (arguments.length>0) node.error.apply(node,arguments) }

		// ---------------------------------------------------------
 		// this section handles the overriding 'config' if option coming in msg.payload
 		// ---------------------------------------------------------
            // override 'start if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('start' in msg.payload) ) {
                if ( validFolderName(msg.payload.start) ) {
                    node.start = msg.payload.start
               }
            }
            // override file 'pattern' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('pattern' in msg.payload) ) {
                if ( (typeof msg.payload.pattern === 'string') && (msg.payload.pattern.length < 1024) ) {
                    node.pattern = msg.payload.pattern
                }
            }
            // override 'folders' pattern if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('folders' in msg.payload) ) {
                if ( (typeof msg.payload.folders === 'string') && (msg.payload.folders.length < 1024) ) {
                    node.folders = msg.payload.folders
                }
            }
            // override 'lstype' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('lstype' in msg.payload) ) {
                if ( (typeof msg.payload.lstype === 'string') && (msg.payload.lstype.length < 50) ) {
                    node.lstype = msg.payload.lstype.toLowerCase()
                }
            }
            // override 'hidden' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('hidden' in msg.payload) ) {
                if ( (typeof msg.payload.hidden === 'boolean')  ) {
                    node.hidden = msg.payload.hidden
                }
            }
            // override 'hidden' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('hidden' in msg.payload) ) {
                if ( (typeof msg.payload.hidden === 'boolean')  ) {
                    node.hidden = msg.payload.hidden
                }
            }
            // override 'depth' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('depth' in msg.payload) ) {
                if ( (typeof msg.payload.depth === 'number') && (msg.payload.depth < 10) ) {
                    node.depth = msg.payload.depth
                }
            }

            var clonedMsg = RED.util.cloneMessage(msg)
            // Remove original _msgid
            delete clonedMsg._msgid
            // Output the nodes config
            clonedMsg.config = {
                'start': node.start,
                'pattern': node.pattern,
                'folders': node.folders,
                'lstype': node.lstype,
                'hidden': node.hidden, /** @since v1.0.2 */
                'path': node.path,
                'single': node.single,
                'depth': node.depth,
                'stat': node.stat
            }

            // Keep count of files found
            var totalFiles = 0

            var options = {}

            if (node.lstype == 'both') {node.lstype = 'files_directories'}
           	options.type = node.lstype

            if ( node.depth > -1 ) {
                options.depth = Number(node.depth)
            }
            if ( node.stat ) options.alwaysStat = true
            /** Show hidden files/folders? Unless explicitly asked for, readdirp will ignore them
             * NB: doesn't help with Windows hidden files/folders
             **/
            node.pattern = node.pattern.replace(/ /g,"")
             if ( node.hidden === true ) {
                // No need for this if supplied patter starts with a dot
                if (node.pattern.charAt(0) !== '.') {
                    var np = node.pattern.replace(/,/g,",.")
                    node.pattern = node.pattern +",."+np
                }
                if (node.folders.charAt(0) !== '.') {
                    var nf = node.folders.replace(/,/g,",.")
                    node.folders = node.folders +",."+nf
                }
            }

	    // change shashes (/) to commas (,) then get rid of extra spaces
            node.folders = node.folders.replace(/\//g,",")
            node.folders = node.folders.replace(/ /g,"")

	    // split the file and directory options into arrays arguments for 'readdirp'
            options.fileFilter = node.pattern.split(',')
            options.directoryFilter = node.folders.split(',')

            var arrayOut = []

            // Recursively read the folder using the stream API
            // @ts-ignore
            readdirp(path.join(node.start), options)
                // Called if a found entry cannot be accessed (e.g. permissions)
                .on('warn', (err) => {
                    node.warn('File could not be accessed', err)
                })
                // Fatal error
                .on('error', (err) => {
                    node.error('Error processing folder listing', err)
                    // Use done for NR 1.0+
                    done(err)
                })
                // called for each entry found
                .on('data', (entry) => {
                    var file = entry.basename

                    // Do we want just the file name output or the full path?
                    if ( node.path ) {
                        file = entry.fullPath
                    }
                    // Do we want to include file stats?
                    if ( node.stat ) {
                        file = {name: file}
                        try {
                            /** NB: some stats props are BigInt which cannot be serialised (e.g. break debug node)
                             *  so convert to Number but that may truncate the value.
                             * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
                             **/
                            file.stat = {
                                size: Number(entry.stats.size), //in bytes
                                created: entry.stats.birthtime, // @since v1.1.0 ctime no longer used - see Node.js docs
                                changed: entry.stats.mtime,
                                accessed: entry.stats.atime,
                                statusChanged: entry.stats.ctime, // @since v1.1.0
                                uid: Number(entry.stats.uid),
                                gid: Number(entry.stats.gid),
                                mode: '0x' + entry.stats.mode.toString(16)
                            }
                        } catch (err) {
                            file.stat = {'error': 'Cannot stat file', 'err': err}
                        }
                    }
                    // If only single output
                    if ( node.single ) {
                        // accumulate the output array (send later on end)
                        arrayOut.push(file)
                    } else {
                        // or send each file as separate msg
                        node.status({fill:'green',shape:'dot',text:'# files ' + ++totalFiles})
                        send({
                            topic: clonedMsg.topic,
                            origPayload: clonedMsg.origPayload,
                            config: clonedMsg.config,
                            payload: file,
                            // debug: entry
                        })
                    }
                })
                // called when all entries have been output
                .on('end', () => {
                    // Send a single msg if arrayOut contains anything
                    if ( node.single ) {
                        //if ( arrayOut.length > 0 ) {
                        node.status({fill:'blue',shape:'dot',text:'# files ' + arrayOut.length})
                        clonedMsg.payload = arrayOut
                        send(clonedMsg)
                    } else {
                        // Empty folder so output a warning
                        if ( totalFiles === 0 ) {
                            node.status({fill:'yellow',shape:'dot',text:'Empty Folder'})
                            clonedMsg.payload = 'Empty Folder'
                            send(clonedMsg)
                        }
                    }
                    // Finished processing input msg (NR 1+)
                    done()
                })

        }) // --- End of node input fn --- //

    } // --- End of fnFileLister --- //

    // Check if a folder name is safe
    function validFolderName(folder) {
        var isValid = true
        if ( typeof folder !== 'string' ) {
            isValid = false
        } else {
            if ( folder.length > 1023 ) {
                isValid = false
            }
        }
        return isValid
    } // --- End of validFolderName function --- //

    // Register the node by name. This must be called before overriding any of the
    // Node functions.
    RED.nodes.registerType(moduleName, nodeGo)
} // ---- End of Mobule ---- //
<commit_msg>finished adding overrides <commit_after>/**
 * Copyright (c) 2019 Julian Knight (Totally Information)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

// Node for Node-Red that lists all files from a given start folder
// with an optionally specified file extension.
//var fs = require('fs')
var path = require('path')
var readdirp = require('readdirp')

// Module name must match this nodes html file
var moduleName = 'fs-file-lister'

module.exports = function(RED) {
    'use strict'

    // The main node definition - most things happen in here
    function nodeGo(config) {
        // Create a RED node
        RED.nodes.createNode(this, config)

        // copy "this" object in case we need it in context of callbacks of other functions.
        var node = this

        // Store local copies of the node configuration (as defined in the .html)
        node.topic    = config.topic
        node.name     = config.name
        node.start    = config.start
        node.pattern  = config.pattern
        node.folders  = config.folders !== undefined ? config.folders : "*"
        node.lstype   = config.lstype  !== undefined ? config.lstype : "files"
        node.hidden   = config.hidden  !== undefined ? config.hidden : true  /** @since v1.0.2 */
        node.path     = config.path
        node.single   = config.single
        node.depth    = config.depth
        node.stat     = config.stat

		if ( (node.pattern == "") || (node.pattern == "*") ) node.pattern = "*.*"
		if  (node.folders == "") node.folders = "*"

        // Make sure the parameters are strings
        if ( (typeof node.start !== 'string') || (typeof node.pattern !== 'string') ) {
            node.error('Either Folder or Ext is not a string, cannot process. Folder: ' + node.start + ', Pattern: ' + node.ext)
            return
        }

        // respond to inputs....
        /** @since v1.0.1, amended ready for Node-RED v1 */
        node.on('input', function(msg, send, done) {

          // If this is pre-1.0, 'send' will be undefined, so fallback to node.send
            send = send || function() { node.send.apply(node,arguments) }
            // If this is pre-1.0, 'done' will be undefined, so fallback to dummy function
            done = done || function() { if (arguments.length>0) node.error.apply(node,arguments) }

		// ---------------------------------------------------------
 		// this section handles the overriding 'config' if option coming in msg.payload
 		// ---------------------------------------------------------
            // override 'start if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('start' in msg.payload) ) {
                if ( validFolderName(msg.payload.start) ) {
                    node.start = msg.payload.start
               }
            }
            // override file 'pattern' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('pattern' in msg.payload) ) {
                if ( (typeof msg.payload.pattern === 'string') && (msg.payload.pattern.length < 1024) ) {
                    node.pattern = msg.payload.pattern
                }
            }
            // override 'folders' pattern if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('folders' in msg.payload) ) {
                if ( (typeof msg.payload.folders === 'string') && (msg.payload.folders.length < 1024) ) {
                    node.folders = msg.payload.folders
                }
            }
            // override 'lstype' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('lstype' in msg.payload) ) {
                if ( (typeof msg.payload.lstype === 'string') && (msg.payload.lstype.length < 50) ) {
                    node.lstype = msg.payload.lstype.toLowerCase()
                }
            }
            // override 'hidden' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('hidden' in msg.payload) ) {
                if ( (typeof msg.payload.hidden === 'boolean')  ) {
                    node.hidden = msg.payload.hidden
                }
            }
            // override 'path' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('path' in msg.payload) ) {
                if ( (typeof msg.payload.path === 'boolean')  ) {
                    node.path = msg.payload.path
                }
            }
            // override 'single' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('single' in msg.payload) ) {
                if ( (typeof msg.payload.single === 'boolean')  ) {
                    node.single = msg.payload.single
                }
            }
            // override 'depth' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('depth' in msg.payload) ) {
                if ( (typeof msg.payload.depth === 'number') && (msg.payload.depth < 11) ) {
                    node.depth = msg.payload.depth
                }
            }
            // override 'stat' if passed suitable payload
            if ( (typeof msg.payload === 'object') && ('stat' in msg.payload) ) {
                if ( (typeof msg.payload.stat === 'boolean')  ) {
                    node.stat = msg.payload.stat
                }
            }

            var clonedMsg = RED.util.cloneMessage(msg)
            // Remove original _msgid
            delete clonedMsg._msgid
            // Output the nodes config
            clonedMsg.config = {
                'start': node.start,
                'pattern': node.pattern,
                'folders': node.folders,
                'lstype': node.lstype,
                'hidden': node.hidden, /** @since v1.0.2 */
                'path': node.path,
                'single': node.single,
                'depth': node.depth,
                'stat': node.stat
            }

            // Keep count of files found
            var totalFiles = 0

            var options = {}

            if (node.lstype == 'both') {node.lstype = 'files_directories'}
           	options.type = node.lstype

            if ( node.depth > -1 ) {
                options.depth = Number(node.depth)
            }
            if ( node.stat ) options.alwaysStat = true
            /** Show hidden files/folders? Unless explicitly asked for, readdirp will ignore them
             * NB: doesn't help with Windows hidden files/folders
             **/
            node.pattern = node.pattern.replace(/ /g,"")
             if ( node.hidden === true ) {
                // No need for this if supplied patter starts with a dot
                if (node.pattern.charAt(0) !== '.') {
                    var np = node.pattern.replace(/,/g,",.")
                    node.pattern = node.pattern +",."+np
                }
                if (node.folders.charAt(0) !== '.') {
                    var nf = node.folders.replace(/,/g,",.")
                    node.folders = node.folders +",."+nf
                }
            }

//			change shashes (/) to commas (,) then get rid of extra spaces
            node.folders = node.folders.replace(/\//g,",")
            node.folders = node.folders.replace(/ /g,"")

//			split the file and directory options into arrays arguments for 'readdirp'
            options.fileFilter = node.pattern.split(',')
            options.directoryFilter = node.folders.split(',')

            var arrayOut = []

            // Recursively read the folder using the stream API
            // @ts-ignore
            readdirp(path.join(node.start), options)
                // Called if a found entry cannot be accessed (e.g. permissions)
                .on('warn', (err) => {
                    node.warn('File could not be accessed', err)
                })
                // Fatal error
                .on('error', (err) => {
                    node.error('Error processing folder listing', err)
                    // Use done for NR 1.0+
                    done(err)
                })
                // called for each entry found
                .on('data', (entry) => {
                    var file = entry.basename

                    // Do we want just the file name output or the full path?
                    if ( node.path ) {
                        file = entry.fullPath
                    }
                    // Do we want to include file stats?
                    if ( node.stat ) {
                        file = {name: file}
                        try {
                            /** NB: some stats props are BigInt which cannot be serialised (e.g. break debug node)
                             *  so convert to Number but that may truncate the value.
                             * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
                             **/
                            file.stat = {
                                size: Number(entry.stats.size), //in bytes
                                created: entry.stats.birthtime, // @since v1.1.0 ctime no longer used - see Node.js docs
                                changed: entry.stats.mtime,
                                accessed: entry.stats.atime,
                                statusChanged: entry.stats.ctime, // @since v1.1.0
                                uid: Number(entry.stats.uid),
                                gid: Number(entry.stats.gid),
                                mode: '0x' + entry.stats.mode.toString(16)
                            }
                        } catch (err) {
                            file.stat = {'error': 'Cannot stat file', 'err': err}
                        }
                    }
                    // If only single output
                    if ( node.single ) {
                        // accumulate the output array (send later on end)
                        arrayOut.push(file)
                    } else {
                        // or send each file as separate msg
                        node.status({fill:'green',shape:'dot',text:'# files ' + ++totalFiles})
                        send({
                            topic: clonedMsg.topic,
                            origPayload: clonedMsg.origPayload,
                            config: clonedMsg.config,
                            payload: file,
                            // debug: entry
                        })
                    }
                })
                // called when all entries have been output
                .on('end', () => {
                    // Send a single msg if arrayOut contains anything
                    if ( node.single ) {
                        //if ( arrayOut.length > 0 ) {
                        node.status({fill:'blue',shape:'dot',text:'# files ' + arrayOut.length})
                        clonedMsg.payload = arrayOut
                        send(clonedMsg)
                    } else {
                        // Empty folder so output a warning
                        if ( totalFiles === 0 ) {
                            node.status({fill:'yellow',shape:'dot',text:'Empty Folder'})
                            clonedMsg.payload = 'Empty Folder'
                            send(clonedMsg)
                        }
                    }
                    // Finished processing input msg (NR 1+)
                    done()
                })

        }) // --- End of node input fn --- //

    } // --- End of fnFileLister --- //

    // Check if a folder name is safe
    function validFolderName(folder) {
        var isValid = true
        if ( typeof folder !== 'string' ) {
            isValid = false
        } else {
            if ( folder.length > 1023 ) {
                isValid = false
            }
        }
        return isValid
    } // --- End of validFolderName function --- //

    // Register the node by name. This must be called before overriding any of the
    // Node functions.
    RED.nodes.registerType(moduleName, nodeGo)
} // ---- End of Mobule ---- //
<|endoftext|>


<commit_before>import { getDirection } from '../intl/locale';
import overlayElement from './overlay';

const observerConfig = {
  attributes: true,
  characterData: false,
  childList: true,
  subtree: true,
  attributeFilter: ['data-l10n-id', 'data-l10n-args', 'data-l10n-bundle']
};

/**
 * The `LocalizationObserver` class is responsible for localizing DOM trees.
 * It also implements the iterable protocol which allows iterating over and
 * retrieving available `Localization` objects.
 *
 * Each `document` will have its corresponding `LocalizationObserver` instance
 * created automatically on startup, as `document.l10n`.
 */
export default class LocalizationObserver {
  /**
   * @returns {LocalizationObserver}
   */
  constructor() {
    this.localizations = new Map();
    this.roots = new WeakMap();
    this.observer = new MutationObserver(
      mutations => this.translateMutations(mutations)
    );
  }

  /**
   * Test if the `Localization` object with a given name already exists.
   *
   * ```javascript
   * if (document.l10n.has('extra')) {
   *   const extraLocalization = document.l10n.get('extra');
   * }
   * ```
   * @param   {string} name - key for the object
   * @returns {boolean}
   */
  has(name) {
    return this.localizations.has(name);
  }

  /**
   * Retrieve a reference to the `Localization` object by name.
   *
   * ```javascript
   * const mainLocalization = document.l10n.get('main');
   * const extraLocalization = document.l10n.get('extra');
   * ```
   *
   * @param   {string}        name - key for the object
   * @returns {Localization}
   */
  get(name) {
    return this.localizations.get(name);
  }

  /**
   * Sets a reference to the `Localization` object by name.
   *
   * ```javascript
   * const loc = new Localization();
   * document.l10n.set('extra', loc);
   * ```
   *
   * @param   {string}       name - key for the object
   * @param   {Localization} value - `Localization` object
   * @returns {LocalizationObserver}
   */
  set(name, value) {
    this.localizations.set(name, value);
    return this;
  }

  *[Symbol.iterator]() {
    yield* this.localizations;
  }

  handleEvent() {
    return this.requestLanguages();
  }

  /**
   * Trigger the language negotation process with an array of language codes.
   * Returns a promise with the negotiated array of language objects as above.
   *
   * ```javascript
   * document.l10n.requestLanguages(['de-DE', 'de', 'en-US']);
   * ```
   *
   * @param   {Array<string>} requestedLangs - array of requested languages
   * @returns {Promise<Array<string>>}
   */
  requestLanguages(requestedLangs) {
    const localizations = Array.from(this.localizations.values());
    return Promise.all(
      localizations.map(l10n => l10n.requestLanguages(requestedLangs))
    ).then(
      () => this.translateAllRoots()
    )
  }

  /**
   * Set the `data-l10n-id` and `data-l10n-args` attributes on DOM elements.
   * L20n makes use of mutation observers to detect changes to `data-l10n-*`
   * attributes and translate elements asynchronously.  `setAttributes` is
   * a convenience method which allows to translate DOM elements declaratively.
   *
   * You should always prefer to use `data-l10n-id` on elements (statically in
   * HTML or dynamically via `setAttributes`) over manually retrieving
   * translations with `format`.  The use of attributes ensures that the
   * elements can be retranslated when the user changes their language
   * preferences.
   *
   * ```javascript
   * document.l10n.setAttributes(
   *   document.querySelector('#welcome'), 'hello', { who: 'world' }
   * );
   * ```
   *
   * This will set the following attributes on the `#welcome` element.  L20n's
   * MutationObserver will pick up this change and will localize the element
   * asynchronously.
   *
   * ```html
   * <p id='welcome'
   *   data-l10n-id='hello'
   *   data-l10n-args='{"who": "world"}'>
   * </p>
   *
   * @param {Element}             element - Element to set attributes on
   * @param {string}                  id      - l10n-id string
   * @param {Object<string, string>} args    - KVP list of l10n arguments
   * ```
   */
  setAttributes(element, id, args) {
    element.setAttribute('data-l10n-id', id);
    if (args) {
      element.setAttribute('data-l10n-args', JSON.stringify(args));
    }
    return element;
  }

  /**
   * Get the `data-l10n-*` attributes from DOM elements.
   *
   * ```javascript
   * document.l10n.getAttributes(
   *   document.querySelector('#welcome')
   * );
   * // -> { id: 'hello', args: { who: 'world' } }
   * ```
   *
   * @param   {Element}  element - HTML element
   * @returns {{id: string, args: Object}}
   */
  getAttributes(element) {
    return {
      id: element.getAttribute('data-l10n-id'),
      args: JSON.parse(element.getAttribute('data-l10n-args'))
    };
  }

  /**
   * Add a new root to the list of observed ones.
   *
   * @param {Element}      root - Root to observe.
   * @param {Localization} l10n - `Localization` object
   */
  observeRoot(root, l10n = this.get('main')) {
    if (!this.roots.has(l10n)) {
      this.roots.set(l10n, new Set());
    }
    this.roots.get(l10n).add(root);
    this.observer.observe(root, observerConfig);
  }

  /**
   * Remove a root from the list of observed ones.
   * If the root is the last to be associated with a given `Localization` object
   * the `Localization` object association will also be removed.
   *
   * Returns `true` if the root was the last one associated with at least
   * one `Localization` object.
   *
   * @param   {Element} root - Root to disconnect.
   * @returns {boolean}
   */
  disconnectRoot(root) {
    let wasLast = false;

    this.pause();
    for (const [name, l10n] of this.localizations) {
      const roots = this.roots.get(l10n);
      if (roots && roots.has(root)) {
        roots.delete(root);
        if (roots.size === 0) {
          wasLast = true;
          this.localizations.delete(name);
          this.roots.delete(l10n);
        }
      }
    }
    this.resume();

    return wasLast;
  }

  /**
   * Pauses the `MutationObserver`
   */
  pause() {
    this.observer.disconnect();
  }

  /**
   * Resumes the `MutationObserver`
   */
  resume() {
    for (const l10n of this.localizations.values()) {
      if (this.roots.has(l10n)) {
        for (const root of this.roots.get(l10n)) {
          this.observer.observe(root, observerConfig)
        }
      }
    }
  }

  /**
   * Triggers translation of all roots associated with the
   * `LocalizationObserver`.
   *
   * Returns a `Promise` which is resolved once all translations are
   * completed.
   *
   * @returns {Promise}
   */
  translateAllRoots() {
    const localizations = Array.from(this.localizations.values());
    return Promise.all(
      localizations.map(
        l10n => this.translateRoots(l10n)
      )
    );
  }

  translateRoots(l10n) {
    if (!this.roots.has(l10n)) {
      return Promise.resolve();
    }

    const roots = Array.from(this.roots.get(l10n));
    return Promise.all(
      roots.map(root => this.translateRoot(root, l10n))
    );
  }

  translateRoot(root, l10n) {
    function setLangs() {
      return l10n.interactive.then(bundles => {
        const langs = bundles.map(bundle => bundle.lang);
        const wasLocalizedBefore = root.hasAttribute('langs');

        root.setAttribute('langs', langs.join(' '));
        root.setAttribute('lang', langs[0]);
        root.setAttribute('dir', getDirection(langs[0]));

        if (wasLocalizedBefore) {
          root.dispatchEvent(new CustomEvent('DOMRetranslated', {
            bubbles: false,
            cancelable: false,
          }));
        }
      });
    }

    return this.translateRootContent(root).then(setLangs);
  }

  translateRootContent(root) {
    markStart();

    const anonChildren = document.getAnonymousNodes ?
      document.getAnonymousNodes(root) : null;
    if (!anonChildren) {
      return this.translateFragment(root).then(markEnd);
    }

    return Promise.all(
      [root, ...anonChildren].map(node => this.translateFragment(node))
    ).then(markEnd);
  }

  translateMutations(mutations) {
    for (const mutation of mutations) {
      switch (mutation.type) {
        case 'attributes':
          this.translateElement(mutation.target);
          break;
        case 'childList':
          for (const addedNode of mutation.addedNodes) {
            if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
              if (addedNode.childElementCount) {
                this.translateFragment(addedNode);
              } else if (addedNode.hasAttribute('data-l10n-id')) {
                this.translateElement(addedNode);
              }
            }
          }
          break;
      }
    }
  }

  /**
   * Translate a DOM node or fragment asynchronously.
   *
   * You can manually trigger translation (or re-translation) of a DOM fragment
   * with `translateFragment`.  Use the `data-l10n-id` and `data-l10n-args`
   * attributes to mark up the DOM with information about which translations to
   * use.
   *
   * Returns a `Promise` that gets resolved once the translation is complete.
   *
   * @param   {DOMFragment} frag - DOMFragment to be translated
   * @returns {Promise}
   */
  translateFragment(frag) {
    return Promise.all(
      this.groupTranslatablesByLocalization(frag).map(
        elemsWithL10n => this.translateElements(
          elemsWithL10n[0], elemsWithL10n[1]
        )
      )
    );
  }

  translateElements(l10n, elements) {
    if (!elements.length) {
      return [];
    }

    const keys = elements.map(this.getKeysForElement);
    return l10n.formatEntities(keys).then(
      translations => this.applyTranslations(elements, translations)
    );
  }

  /**
   * Translates a single DOM node asynchronously.
   *
   * Returns a `Promise` that gets resolved once the translation is complete.
   *
   * @param   {Element} element - HTML element to be translated
   * @returns {Promise}
   */
  translateElement(element) {
    const l10n = this.get(element.getAttribute('data-l10n-bundle') || 'main');
    return l10n.formatEntities([this.getKeysForElement(element)]).then(
      translations => this.applyTranslations([element], translations)
    );
  }

  applyTranslations(elements, translations) {
    this.pause();
    for (let i = 0; i < elements.length; i++) {
      overlayElement(elements[i], translations[i]);
    }
    this.resume();
  }

  groupTranslatablesByLocalization(frag) {
    const elemsWithL10n = [];
    for (const loc of this.localizations) {
      elemsWithL10n.push(
        [loc[1], this.getTranslatables(frag, loc[0])]
      );
    }
    return elemsWithL10n;
  }

  getTranslatables(element, bundleName) {
    const query = bundleName === 'main' ?
      '[data-l10n-bundle="main"], [data-l10n-id]:not([data-l10n-bundle])' :
      `[data-l10n-bundle=${bundleName}]`;
    const nodes = Array.from(element.querySelectorAll(query));

    if (typeof element.hasAttribute === 'function' &&
        element.hasAttribute('data-l10n-id')) {
      const elemBundleName = element.getAttribute('data-l10n-bundle');
      if (elemBundleName === null || elemBundleName === bundleName) {
        nodes.push(element);
      }
    }

    return nodes;
  }

  getKeysForElement(element) {
    return [
      element.getAttribute('data-l10n-id'),
      // In XUL documents missing attributes return `''` here which breaks
      // JSON.parse.  HTML documents return `null`.
      JSON.parse(element.getAttribute('data-l10n-args') || null)
    ];
  }
}

function markStart() {
  performance.mark('l20n: start translateRootContent');
}

function markEnd() {
  performance.mark('l20n: end translateRootContent');
  performance.measure(
    'l20n: translateRootContent',
    'l20n: start translateRootContent',
    'l20n: end translateRootContent'
  );
}
<commit_msg>Bug 1312775 - Remove performance.marks from the bindings and l20n-perf-monitor from browser.xul. r=stas<commit_after>import { getDirection } from '../intl/locale';
import overlayElement from './overlay';

const observerConfig = {
  attributes: true,
  characterData: false,
  childList: true,
  subtree: true,
  attributeFilter: ['data-l10n-id', 'data-l10n-args', 'data-l10n-bundle']
};

/**
 * The `LocalizationObserver` class is responsible for localizing DOM trees.
 * It also implements the iterable protocol which allows iterating over and
 * retrieving available `Localization` objects.
 *
 * Each `document` will have its corresponding `LocalizationObserver` instance
 * created automatically on startup, as `document.l10n`.
 */
export default class LocalizationObserver {
  /**
   * @returns {LocalizationObserver}
   */
  constructor() {
    this.localizations = new Map();
    this.roots = new WeakMap();
    this.observer = new MutationObserver(
      mutations => this.translateMutations(mutations)
    );
  }

  /**
   * Test if the `Localization` object with a given name already exists.
   *
   * ```javascript
   * if (document.l10n.has('extra')) {
   *   const extraLocalization = document.l10n.get('extra');
   * }
   * ```
   * @param   {string} name - key for the object
   * @returns {boolean}
   */
  has(name) {
    return this.localizations.has(name);
  }

  /**
   * Retrieve a reference to the `Localization` object by name.
   *
   * ```javascript
   * const mainLocalization = document.l10n.get('main');
   * const extraLocalization = document.l10n.get('extra');
   * ```
   *
   * @param   {string}        name - key for the object
   * @returns {Localization}
   */
  get(name) {
    return this.localizations.get(name);
  }

  /**
   * Sets a reference to the `Localization` object by name.
   *
   * ```javascript
   * const loc = new Localization();
   * document.l10n.set('extra', loc);
   * ```
   *
   * @param   {string}       name - key for the object
   * @param   {Localization} value - `Localization` object
   * @returns {LocalizationObserver}
   */
  set(name, value) {
    this.localizations.set(name, value);
    return this;
  }

  *[Symbol.iterator]() {
    yield* this.localizations;
  }

  handleEvent() {
    return this.requestLanguages();
  }

  /**
   * Trigger the language negotation process with an array of language codes.
   * Returns a promise with the negotiated array of language objects as above.
   *
   * ```javascript
   * document.l10n.requestLanguages(['de-DE', 'de', 'en-US']);
   * ```
   *
   * @param   {Array<string>} requestedLangs - array of requested languages
   * @returns {Promise<Array<string>>}
   */
  requestLanguages(requestedLangs) {
    const localizations = Array.from(this.localizations.values());
    return Promise.all(
      localizations.map(l10n => l10n.requestLanguages(requestedLangs))
    ).then(
      () => this.translateAllRoots()
    )
  }

  /**
   * Set the `data-l10n-id` and `data-l10n-args` attributes on DOM elements.
   * L20n makes use of mutation observers to detect changes to `data-l10n-*`
   * attributes and translate elements asynchronously.  `setAttributes` is
   * a convenience method which allows to translate DOM elements declaratively.
   *
   * You should always prefer to use `data-l10n-id` on elements (statically in
   * HTML or dynamically via `setAttributes`) over manually retrieving
   * translations with `format`.  The use of attributes ensures that the
   * elements can be retranslated when the user changes their language
   * preferences.
   *
   * ```javascript
   * document.l10n.setAttributes(
   *   document.querySelector('#welcome'), 'hello', { who: 'world' }
   * );
   * ```
   *
   * This will set the following attributes on the `#welcome` element.  L20n's
   * MutationObserver will pick up this change and will localize the element
   * asynchronously.
   *
   * ```html
   * <p id='welcome'
   *   data-l10n-id='hello'
   *   data-l10n-args='{"who": "world"}'>
   * </p>
   *
   * @param {Element}             element - Element to set attributes on
   * @param {string}                  id      - l10n-id string
   * @param {Object<string, string>} args    - KVP list of l10n arguments
   * ```
   */
  setAttributes(element, id, args) {
    element.setAttribute('data-l10n-id', id);
    if (args) {
      element.setAttribute('data-l10n-args', JSON.stringify(args));
    }
    return element;
  }

  /**
   * Get the `data-l10n-*` attributes from DOM elements.
   *
   * ```javascript
   * document.l10n.getAttributes(
   *   document.querySelector('#welcome')
   * );
   * // -> { id: 'hello', args: { who: 'world' } }
   * ```
   *
   * @param   {Element}  element - HTML element
   * @returns {{id: string, args: Object}}
   */
  getAttributes(element) {
    return {
      id: element.getAttribute('data-l10n-id'),
      args: JSON.parse(element.getAttribute('data-l10n-args'))
    };
  }

  /**
   * Add a new root to the list of observed ones.
   *
   * @param {Element}      root - Root to observe.
   * @param {Localization} l10n - `Localization` object
   */
  observeRoot(root, l10n = this.get('main')) {
    if (!this.roots.has(l10n)) {
      this.roots.set(l10n, new Set());
    }
    this.roots.get(l10n).add(root);
    this.observer.observe(root, observerConfig);
  }

  /**
   * Remove a root from the list of observed ones.
   * If the root is the last to be associated with a given `Localization` object
   * the `Localization` object association will also be removed.
   *
   * Returns `true` if the root was the last one associated with at least
   * one `Localization` object.
   *
   * @param   {Element} root - Root to disconnect.
   * @returns {boolean}
   */
  disconnectRoot(root) {
    let wasLast = false;

    this.pause();
    for (const [name, l10n] of this.localizations) {
      const roots = this.roots.get(l10n);
      if (roots && roots.has(root)) {
        roots.delete(root);
        if (roots.size === 0) {
          wasLast = true;
          this.localizations.delete(name);
          this.roots.delete(l10n);
        }
      }
    }
    this.resume();

    return wasLast;
  }

  /**
   * Pauses the `MutationObserver`
   */
  pause() {
    this.observer.disconnect();
  }

  /**
   * Resumes the `MutationObserver`
   */
  resume() {
    for (const l10n of this.localizations.values()) {
      if (this.roots.has(l10n)) {
        for (const root of this.roots.get(l10n)) {
          this.observer.observe(root, observerConfig)
        }
      }
    }
  }

  /**
   * Triggers translation of all roots associated with the
   * `LocalizationObserver`.
   *
   * Returns a `Promise` which is resolved once all translations are
   * completed.
   *
   * @returns {Promise}
   */
  translateAllRoots() {
    const localizations = Array.from(this.localizations.values());
    return Promise.all(
      localizations.map(
        l10n => this.translateRoots(l10n)
      )
    );
  }

  translateRoots(l10n) {
    if (!this.roots.has(l10n)) {
      return Promise.resolve();
    }

    const roots = Array.from(this.roots.get(l10n));
    return Promise.all(
      roots.map(root => this.translateRoot(root, l10n))
    );
  }

  translateRoot(root, l10n) {
    function setLangs() {
      return l10n.interactive.then(bundles => {
        const langs = bundles.map(bundle => bundle.lang);
        const wasLocalizedBefore = root.hasAttribute('langs');

        root.setAttribute('langs', langs.join(' '));
        root.setAttribute('lang', langs[0]);
        root.setAttribute('dir', getDirection(langs[0]));

        if (wasLocalizedBefore) {
          root.dispatchEvent(new CustomEvent('DOMRetranslated', {
            bubbles: false,
            cancelable: false,
          }));
        }
      });
    }

    return this.translateRootContent(root).then(setLangs);
  }

  translateRootContent(root) {
    const anonChildren = document.getAnonymousNodes ?
      document.getAnonymousNodes(root) : null;
    if (!anonChildren) {
      return this.translateFragment(root);
    }

    return Promise.all(
      [root, ...anonChildren].map(node => this.translateFragment(node))
    );
  }

  translateMutations(mutations) {
    for (const mutation of mutations) {
      switch (mutation.type) {
        case 'attributes':
          this.translateElement(mutation.target);
          break;
        case 'childList':
          for (const addedNode of mutation.addedNodes) {
            if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
              if (addedNode.childElementCount) {
                this.translateFragment(addedNode);
              } else if (addedNode.hasAttribute('data-l10n-id')) {
                this.translateElement(addedNode);
              }
            }
          }
          break;
      }
    }
  }

  /**
   * Translate a DOM node or fragment asynchronously.
   *
   * You can manually trigger translation (or re-translation) of a DOM fragment
   * with `translateFragment`.  Use the `data-l10n-id` and `data-l10n-args`
   * attributes to mark up the DOM with information about which translations to
   * use.
   *
   * Returns a `Promise` that gets resolved once the translation is complete.
   *
   * @param   {DOMFragment} frag - DOMFragment to be translated
   * @returns {Promise}
   */
  translateFragment(frag) {
    return Promise.all(
      this.groupTranslatablesByLocalization(frag).map(
        elemsWithL10n => this.translateElements(
          elemsWithL10n[0], elemsWithL10n[1]
        )
      )
    );
  }

  translateElements(l10n, elements) {
    if (!elements.length) {
      return [];
    }

    const keys = elements.map(this.getKeysForElement);
    return l10n.formatEntities(keys).then(
      translations => this.applyTranslations(elements, translations)
    );
  }

  /**
   * Translates a single DOM node asynchronously.
   *
   * Returns a `Promise` that gets resolved once the translation is complete.
   *
   * @param   {Element} element - HTML element to be translated
   * @returns {Promise}
   */
  translateElement(element) {
    const l10n = this.get(element.getAttribute('data-l10n-bundle') || 'main');
    return l10n.formatEntities([this.getKeysForElement(element)]).then(
      translations => this.applyTranslations([element], translations)
    );
  }

  applyTranslations(elements, translations) {
    this.pause();
    for (let i = 0; i < elements.length; i++) {
      overlayElement(elements[i], translations[i]);
    }
    this.resume();
  }

  groupTranslatablesByLocalization(frag) {
    const elemsWithL10n = [];
    for (const loc of this.localizations) {
      elemsWithL10n.push(
        [loc[1], this.getTranslatables(frag, loc[0])]
      );
    }
    return elemsWithL10n;
  }

  getTranslatables(element, bundleName) {
    const query = bundleName === 'main' ?
      '[data-l10n-bundle="main"], [data-l10n-id]:not([data-l10n-bundle])' :
      `[data-l10n-bundle=${bundleName}]`;
    const nodes = Array.from(element.querySelectorAll(query));

    if (typeof element.hasAttribute === 'function' &&
        element.hasAttribute('data-l10n-id')) {
      const elemBundleName = element.getAttribute('data-l10n-bundle');
      if (elemBundleName === null || elemBundleName === bundleName) {
        nodes.push(element);
      }
    }

    return nodes;
  }

  getKeysForElement(element) {
    return [
      element.getAttribute('data-l10n-id'),
      // In XUL documents missing attributes return `''` here which breaks
      // JSON.parse.  HTML documents return `null`.
      JSON.parse(element.getAttribute('data-l10n-args') || null)
    ];
  }
}
<|endoftext|>


<commit_before>quizApp.current.data = {
  type: "report",
  text: "This are your results:"
}
<commit_msg>fix type<commit_after>quizApp.current.data = {
  type: "report",
  text: "These are your results:"
}
<|endoftext|>