diff --git a/client/client/routes.json b/client/client/routes.json
index 896ab0cad..d8d0fbc03 100644
--- a/client/client/routes.json
+++ b/client/client/routes.json
@@ -9,16 +9,19 @@
"state": "clients",
"component": "vn-client-index",
"acl": ["employee"]
- }, {
+ },
+ {
"url": "/create",
"state": "create",
"component": "vn-client-create"
- }, {
+ },
+ {
"url": "/clients/:id",
"state": "clientCard",
"abstract": true,
"component": "vn-client-card"
- }, {
+ },
+ {
"url": "/basic-data",
"state": "clientCard.basicData",
"component": "vn-client-basic-data",
@@ -29,7 +32,8 @@
"description": "Basic data",
"icon": "person"
}
- }, {
+ },
+ {
"url": "/fiscal-data",
"state": "clientCard.fiscalData",
"component": "vn-client-fiscal-data",
@@ -40,7 +44,8 @@
"description": "Fiscal data",
"icon": "account_balance"
}
- }, {
+ },
+ {
"url": "/billing-data",
"state": "clientCard.billingData",
"component": "vn-client-billing-data",
@@ -51,12 +56,14 @@
"description": "Pay method",
"icon": "assignment"
}
- }, {
+ },
+ {
"url": "/addresses",
"state": "clientCard.addresses",
"component": "ui-view",
"abstract": true
- }, {
+ },
+ {
"url": "/list",
"state": "clientCard.addresses.list",
"component": "vn-client-addresses",
@@ -67,15 +74,18 @@
"description": "Addresses",
"icon": "local_shipping"
}
- }, {
+ },
+ {
"url": "/create",
"state": "clientCard.addresses.create",
"component": "vn-address-create"
- }, {
+ },
+ {
"url": "/:addressId/edit",
"state": "clientCard.addresses.edit",
"component": "vn-address-edit"
- }, {
+ },
+ {
"url": "/web-access",
"state": "clientCard.webAccess",
"component": "vn-client-web-access",
@@ -86,12 +96,14 @@
"description": "Web access",
"icon": "language"
}
- }, {
+ },
+ {
"url": "/notes",
"state": "clientCard.notes",
"component": "ui-view",
"abstract": true
- }, {
+ },
+ {
"url": "/list",
"state": "clientCard.notes.list",
"component": "vn-client-notes",
@@ -102,16 +114,19 @@
"description": "Notes",
"icon": "insert_drive_file"
}
- }, {
+ },
+ {
"url": "/create",
"state": "clientCard.notes.create",
"component": "vn-note-create"
- }, {
+ },
+ {
"url": "/credit",
"abstract": true,
"state": "clientCard.credit",
"component": "ui-view"
- }, {
+ },
+ {
"url": "/list",
"state": "clientCard.credit.list",
"component": "vn-client-credit-list",
@@ -123,19 +138,22 @@
"icon": "credit_card"
},
"acl": ["manager", "salesAssistant", "teamBoss", "teamManager"]
- }, {
+ },
+ {
"url": "/create",
"state": "clientCard.credit.create",
"component": "vn-client-credit-create",
"params": {
"client": "$ctrl.client"
}
- }, {
+ },
+ {
"url": "/greuge",
"abstract": true,
"state": "clientCard.greuge",
"component": "ui-view"
- }, {
+ },
+ {
"url": "/list",
"state": "clientCard.greuge.list",
"component": "vn-client-greuge-list",
@@ -146,14 +164,16 @@
"description": "Greuge",
"icon": "work"
}
- }, {
+ },
+ {
"url": "/create",
"state": "clientCard.greuge.create",
"component": "vn-client-greuge-create",
"params": {
"client": "$ctrl.client"
}
- }, {
+ },
+ {
"url": "/mandate",
"state": "clientCard.mandate",
"component": "vn-client-mandate",
diff --git a/client/item/routes.json b/client/item/routes.json
index 863a8a4b9..127c1e84f 100644
--- a/client/item/routes.json
+++ b/client/item/routes.json
@@ -15,16 +15,19 @@
"state": "item.index",
"component": "vn-item-list",
"acl": ["developer"]
- }, {
+ },
+ {
"url": "/create",
"state": "item.create",
"component": "vn-item-create"
- }, {
+ },
+ {
"url": "/:id",
"state": "item.card",
"abstract": true,
"component": "vn-item-card"
- }, {
+ },
+ {
"url" : "/data",
"state": "item.card.data",
"component": "vn-item-data",
@@ -35,7 +38,8 @@
"description": "Basic data",
"icon": "folder"
}
- },{
+ },
+ {
"url" : "/tags",
"state": "item.card.tags",
"component": "vn-item-tags",
@@ -46,7 +50,8 @@
"description": "Tags",
"icon": "folder"
}
- },{
+ },
+ {
"url" : "/history",
"state": "item.card.history",
"component": "vn-item-history",
@@ -57,7 +62,8 @@
"description": "History",
"icon": "folder"
}
- },{
+ },
+ {
"url" : "/niche",
"state": "item.card.niche",
"component": "vn-item-niche",
@@ -68,7 +74,8 @@
"description": "Niche",
"icon": "folder"
}
- },{
+ },
+ {
"url" : "/botanical",
"state": "item.card.botanical",
"component": "vn-item-botanical",
@@ -81,7 +88,7 @@
}
},{
"url" : "/barcode",
- "state": "item.card.barcode",
+ "state": "item.card.itemBarcode",
"component": "vn-item-barcode",
"params": {
"item": "$ctrl.item"
diff --git a/client/item/src/barcode/item-barcode.html b/client/item/src/barcode/item-barcode.html
index 549ac04d1..9f01f4c64 100644
--- a/client/item/src/barcode/item-barcode.html
+++ b/client/item/src/barcode/item-barcode.html
@@ -1,5 +1,32 @@
-
-
- Item barcode
-
-
\ No newline at end of file
+
diff --git a/client/item/src/barcode/item-barcode.js b/client/item/src/barcode/item-barcode.js
index 850c262b8..eac4ab6cb 100644
--- a/client/item/src/barcode/item-barcode.js
+++ b/client/item/src/barcode/item-barcode.js
@@ -1,5 +1,127 @@
import ngModule from '../module';
+export default class Controller {
+ constructor($state, $scope, $http, $q, $translate, vnApp) {
+ this.$state = $state;
+ this.$scope = $scope;
+ this.$http = $http;
+ this.$q = $q;
+ this.$translate = $translate;
+ this.vnApp = vnApp;
+
+ this.barcodes = [];
+ this.removedBarcodes = [];
+ this.oldBarcodes = {};
+ }
+ _setIconAdd() {
+ if (this.barcodes.length) {
+ this.barcodes.map(element => {
+ element.showAddIcon = false;
+ return true;
+ });
+ this.barcodes[this.barcodes.length - 1].showAddIcon = true;
+ } else {
+ this.addBarcode();
+ }
+ }
+
+ _setDirtyForm() {
+ if (this.$scope.form) {
+ this.$scope.form.$setDirty();
+ }
+ }
+ _unsetDirtyForm() {
+ if (this.$scope.form) {
+ this.$scope.form.$setPristine();
+ }
+ }
+ _equalBarcodes(oldBarcode, newBarcode) {
+ return oldBarcode.id === newBarcode.id && oldBarcode.code === newBarcode.code;
+ }
+
+ addBarcode() {
+ this.barcodes.push({code: null, itemFk: this.$state.params.id, showAddIcon: true});
+ this._setIconAdd();
+ }
+
+ removeBarcode(index) {
+ let item = this.barcodes[index];
+ if (item) {
+ this.barcodes.splice(index, 1);
+ this._setIconAdd();
+ if (item.id) {
+ this.removedBarcodes.push(item.id);
+ this._setDirtyForm();
+ }
+ }
+ }
+
+ submit() {
+ let codes = [];
+ let repeatedBarcodes = false;
+ let canSubmit;
+ let barcodesObj = {
+ delete: this.removedBarcodes,
+ create: [],
+ update: []
+ };
+ for (let i = 0; i < this.barcodes.length; i++) {
+ let barcode = this.barcodes[i];
+ let isNewBarcode = barcode.id === undefined;
+
+ if (barcode.code && codes.indexOf(barcode.code) !== -1) {
+ repeatedBarcodes = true;
+ break;
+ }
+ if (barcode.code) codes.push(barcode.code);
+
+ if (isNewBarcode && barcode.code) {
+ barcodesObj.create.push(barcode);
+ } else if (!isNewBarcode && !this._equalBarcodes(this.oldBarcodes[barcode.id], barcode)) {
+ barcodesObj.update.push(barcode);
+ }
+ }
+
+ if (repeatedBarcodes) {
+ return this.vnApp.showMessage(this.$translate.instant('The barcode must be unique'));
+ }
+
+ canSubmit = barcodesObj.update.length > 0 || barcodesObj.create.length > 0 || barcodesObj.delete.length > 0;
+
+ if (canSubmit) {
+ return this.$http.post(`/item/api/ItemBarcodes/crudItemBarcodes`, barcodesObj).then(() => {
+ this.getBarcodes();
+ this._unsetDirtyForm();
+ });
+ }
+ this.vnApp.showMessage(this.$translate.instant('No changes to save'));
+ }
+
+ setOldBarcodes(response) {
+ this._setIconAdd();
+ response.data.forEach(barcode => {
+ this.oldBarcodes[barcode.id] = Object.assign({}, barcode);
+ });
+ }
+
+ getBarcodes() {
+ let filter = {
+ where: {itemFk: this.$state.params.id}
+ };
+ this.$http.get(`/item/api/ItemBarcodes?filter=${JSON.stringify(filter)}`).then(response => {
+ this.barcodes = response.data;
+ this.setOldBarcodes(response);
+ });
+ }
+
+ $onInit() {
+ this.getBarcodes();
+ }
+}
+
+Controller.$inject = ['$state', '$scope', '$http', '$q', '$translate', 'vnApp'];
+
ngModule.component('vnItemBarcode', {
- template: require('./item-barcode.html')
+ template: require('./item-barcode.html'),
+ controller: Controller
});
diff --git a/client/item/src/barcode/item-barcode.spec.js b/client/item/src/barcode/item-barcode.spec.js
new file mode 100644
index 000000000..98e645c26
--- /dev/null
+++ b/client/item/src/barcode/item-barcode.spec.js
@@ -0,0 +1,142 @@
+import './item-barcode.js';
+
+describe('Item', () => {
+ describe('Component vnItemBarcode', () => {
+ let $componentController;
+ let $state;
+ let controller;
+ let $httpBackend;
+
+ beforeEach(() => {
+ angular.mock.module('item');
+ });
+
+ beforeEach(angular.mock.inject((_$componentController_, _$state_, _$httpBackend_) => {
+ $componentController = _$componentController_;
+ $state = _$state_;
+ $httpBackend = _$httpBackend_;
+ $state.params.id = '1';
+ controller = $componentController('vnItemBarcode', {$state: $state});
+ }));
+
+ describe('add / remove barcode()', () => {
+ it('should add one empty barcode into controller barcodes collection and call _setIconAdd()', () => {
+ controller.barcodes = [];
+ spyOn(controller, '_setIconAdd').and.callThrough();
+ controller.addBarcode();
+
+ expect(controller._setIconAdd).toHaveBeenCalledWith();
+ expect(controller.barcodes.length).toEqual(1);
+ expect(controller.barcodes[0].id).toBe(undefined);
+ expect(controller.barcodes[0].showAddIcon).toBeTruthy();
+ });
+
+ it('should remove a barcode that occupies the position in the index given and call _setIconAdd()', () => {
+ let index = 2;
+ controller.barcodes = [
+ {id: 1, code: '1111', showAddIcon: false},
+ {id: 2, code: '2222', showAddIcon: false},
+ {id: 3, code: '3333', showAddIcon: true}
+ ];
+
+ spyOn(controller, '_setIconAdd').and.callThrough();
+
+ controller.removeBarcode(index);
+
+ expect(controller._setIconAdd).toHaveBeenCalledWith();
+ expect(controller.barcodes.length).toEqual(2);
+ expect(controller.barcodes[0].showAddIcon).toBeFalsy();
+ expect(controller.barcodes[1].showAddIcon).toBeTruthy();
+ expect(controller.barcodes[index]).toBe(undefined);
+ });
+ });
+
+ describe('_equalBarcodes()', () => {
+ it('should return true if two barcodes are equals independent of control attributes', () => {
+ let code1 = {id: 1, code: '1111', showAddIcon: true};
+ let code2 = {id: 1, code: '1111', showAddIcon: false};
+ let equals = controller._equalBarcodes(code2, code1);
+
+ expect(equals).toBeTruthy();
+ });
+
+ it('should return false if two barcodes aint equals independent of control attributes', () => {
+ let code1 = {id: 1, code: '1111', showAddIcon: true};
+ let code2 = {id: 1, code: '2222', showAddIcon: true};
+ let equals = controller._equalBarcodes(code2, code1);
+
+ expect(equals).toBeFalsy();
+ });
+ });
+
+ describe('getBarcodes()', () => {
+ it('should perform a GET query to receive the item barcodes', () => {
+ let filter = {
+ where: {itemFk: '1'}
+ };
+ let res = [{id: 1, code: '1111'}];
+ $httpBackend.when('GET', `/item/api/ItemBarcodes?filter=${JSON.stringify(filter)}`).respond(res);
+ $httpBackend.expectGET(`/item/api/ItemBarcodes?filter=${JSON.stringify(filter)}`);
+ controller.getBarcodes();
+ $httpBackend.flush();
+ });
+ });
+
+ describe('submit()', () => {
+ it("should return an error message 'The barcode must be unique' when the code isnt unique", () => {
+ spyOn(controller.vnApp, 'showMessage').and.callThrough();
+ controller.barcodes = [
+ {code: 123454, itemFk: 1, id: 1},
+ {code: 123454, itemFk: 1}
+ ];
+ controller.oldBarcodes = {1: {id: 1, code: 123454, itemFk: 1}};
+ controller.submit();
+
+ expect(controller.vnApp.showMessage).toHaveBeenCalledWith('The barcode must be unique');
+ });
+
+ it("should perfom a query to delete barcodes", () => {
+ controller.removedBarcodes = [1, 2, 3];
+
+ $httpBackend.when('GET', `/item/api/ItemBarcodes?filter={"where":{"itemFk":"1"}}`).respond([{code: 123454, itemFk: 1, id: 1}]);
+ $httpBackend.when('POST', `/item/api/ItemBarcodes/crudItemBarcodes`).respond('ok!');
+ $httpBackend.expectPOST(`/item/api/ItemBarcodes/crudItemBarcodes`);
+ controller.submit();
+ $httpBackend.flush();
+ });
+
+ it("should perfom a query to update barcodes", () => {
+ controller.barcodes = [{code: 2222, itemFk: 1, id: 1}];
+ controller.oldBarcodes = {1: {id: 1, code: 1111, itemFk: 1}};
+
+ $httpBackend.when('GET', `/item/api/ItemBarcodes?filter={"where":{"itemFk":"1"}}`).respond([{}]);
+ $httpBackend.when('POST', `/item/api/ItemBarcodes/crudItemBarcodes`).respond('ok!');
+ $httpBackend.expectPOST(`/item/api/ItemBarcodes/crudItemBarcodes`);
+ controller.submit();
+ $httpBackend.flush();
+ });
+
+ it("should perfom a query to create new barcode", () => {
+ controller.barcodes = [{code: 1111, itemFk: 1}];
+
+ $httpBackend.when('GET', `/item/api/ItemBarcodes?filter={"where":{"itemFk":"1"}}`).respond([{}]);
+ $httpBackend.when('POST', `/item/api/ItemBarcodes/crudItemBarcodes`).respond('ok!');
+ $httpBackend.expectPOST(`/item/api/ItemBarcodes/crudItemBarcodes`);
+ controller.submit();
+ $httpBackend.flush();
+ });
+
+ it("should return a message 'No changes to save' when there are no changes to apply", () => {
+ spyOn(controller.vnApp, 'showMessage').and.callThrough();
+ controller.oldBarcodes = [
+ {code: 1, itemFk: 1, id: 1},
+ {code: 2, itemFk: 1, id: 2}
+ ];
+ controller.barcodes = [];
+ controller.submit();
+
+ expect(controller.vnApp.showMessage).toHaveBeenCalledWith('No changes to save');
+ });
+ });
+ });
+});
diff --git a/gulpfile.js b/gulpfile.js
index 05bcc6b76..af1ecc636 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -76,6 +76,22 @@ gulp.task('e2e-only', () => {
.pipe(jasmine({reporter: 'none'}));
});
+/**
+ * Runs the backend tests.
+ */
+// gulp.task('test', ['test-only'], async () => {
+// gulp.watch('./services/**/*', ['test-only']);
+// gulp.unwatch('./services/node_modules');
+// });
+
+// gulp.task('test-only', () => {
+// const jasmine = require('gulp-jasmine');
+// gulp.src('./services/loopback/common/**/*[sS]pec.js')
+// .pipe(jasmine(
+// require('./services-test.config')
+// ));
+// });
+
/**
* Cleans all generated project files.
*/
@@ -181,7 +197,7 @@ gulp.task('nginx-start', ['nginx-conf'], async () => {
if (isWindows)
nginxBin = `start /B ${nginxBin}`;
- log(`Application will be at http://${proxyConf.host}:${proxyConf.port}/`);
+ log(`Application will be available at http://${proxyConf.host}:${proxyConf.port}/`);
await execP(`${nginxBin} -c "${nginxConf}" -p "${nginxDir}"`);
});
diff --git a/services-test.config.js b/services-test.config.js
new file mode 100644
index 000000000..aa8866fc8
--- /dev/null
+++ b/services-test.config.js
@@ -0,0 +1,24 @@
+// const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
+
+// module.exports = {
+// reporter: new SpecReporter({
+// spec: {
+// // displayStacktrace: 'summary',
+// displaySuccessful: false,
+// displayFailedSpec: true,
+// displaySpecDuration: true
+// }
+// }),
+// config: {
+// spec_dir: 'services',
+// spec_files: [
+// // '**/*.spec.js',
+// 'auth/server/**/*.spec.js',
+// 'client/common/**/*.spec.js',
+// 'loopback/common/**/*.spec.js'
+// ],
+// helpers: [
+// '/services/utils/jasmineHelpers.js'
+// ]
+// }
+// };
diff --git a/services/client/common/methods/address/crudAddressObservations.js b/services/client/common/methods/address/crudAddressObservations.js
index b158b0749..0b80d1bac 100644
--- a/services/client/common/methods/address/crudAddressObservations.js
+++ b/services/client/common/methods/address/crudAddressObservations.js
@@ -11,9 +11,6 @@ module.exports = Self => {
http: {source: 'body'}
}
],
- returns: {
- arg: 'sumAmount'
- },
http: {
path: `/crudAddressObservations`,
verb: 'post'
diff --git a/services/item/common/methods/item/crudItemBarcodes.js b/services/item/common/methods/item/crudItemBarcodes.js
new file mode 100644
index 000000000..93eedf133
--- /dev/null
+++ b/services/item/common/methods/item/crudItemBarcodes.js
@@ -0,0 +1,36 @@
+module.exports = Self => {
+ Self.remoteMethod('crudItemBarcodes', {
+ description: 'create, update or delete barcodes',
+ accessType: 'WRITE',
+ accepts: [
+ {
+ arg: 'barcodes',
+ type: 'Object',
+ require: true,
+ description: 'object with barcodes to create, update or delete, Example: {create: [], update: [], delete: []}',
+ http: {source: 'body'}
+ }
+ ],
+ http: {
+ path: `/crudItemBarcodes`,
+ verb: 'post'
+ }
+ });
+
+ Self.crudItemBarcodes = barcodes => {
+ let promises = [];
+
+ if (barcodes.delete && barcodes.delete.length) {
+ promises.push(Self.destroyAll({id: {inq: barcodes.delete}}));
+ }
+ if (barcodes.create.length) {
+ promises.push(Self.create(barcodes.create));
+ }
+ if (barcodes.update.length) {
+ barcodes.update.forEach(barcode => {
+ promises.push(Self.upsert(barcode));
+ });
+ }
+ return Promise.all(promises);
+ };
+};
diff --git a/services/item/common/methods/item/specs/crudItemBarcodes.spec.js b/services/item/common/methods/item/specs/crudItemBarcodes.spec.js
new file mode 100644
index 000000000..72365229a
--- /dev/null
+++ b/services/item/common/methods/item/specs/crudItemBarcodes.spec.js
@@ -0,0 +1,51 @@
+const crudItemBarcodes = require('../crudItemBarcodes');
+const catchErrors = require('../../../../../../services/utils/jasmineHelpers').catchErrors;
+
+describe('Item crudItemBarcodes()', () => {
+ it('should call the destroyAll methodif there are ids in delete Array', done => {
+ let self = jasmine.createSpyObj('self', ['remoteMethod', 'crudItemBarcodes', 'destroyAll', 'create', 'upsert']);
+
+ crudItemBarcodes(self);
+ self.crudItemBarcodes({
+ delete: [1],
+ create: [],
+ update: []
+ }).then(result => {
+ expect(self.destroyAll).toHaveBeenCalledWith({id: {inq: [1]}});
+ done();
+ })
+ .catch(catchErrors(done));
+ });
+
+ it('should call the create method if there are ids in create Array', done => {
+ let self = jasmine.createSpyObj('self', ['remoteMethod', 'crudItemBarcodes', 'destroyAll', 'create', 'upsert']);
+
+ crudItemBarcodes(self);
+ self.crudItemBarcodes({
+ delete: [],
+ create: [1],
+ update: []
+ }).then(result => {
+ expect(self.create).toHaveBeenCalledWith([1]);
+ done();
+ })
+ .catch(catchErrors(done));
+ });
+
+ it('should call the upsert method as many times as ids in update Array', done => {
+ let self = jasmine.createSpyObj('self', ['remoteMethod', 'crudItemBarcodes', 'destroyAll', 'create', 'upsert']);
+
+ crudItemBarcodes(self);
+ self.crudItemBarcodes({
+ delete: [],
+ create: [],
+ update: [1, 2]
+ }).then(result => {
+ expect(self.upsert).toHaveBeenCalledWith(1);
+ expect(self.upsert).toHaveBeenCalledWith(2);
+ expect(self.upsert.calls.count()).toEqual(2);
+ done();
+ })
+ .catch(catchErrors(done));
+ });
+});
diff --git a/services/item/common/models/item.json b/services/item/common/models/item.json
index 5598f371d..a4121e8f5 100644
--- a/services/item/common/models/item.json
+++ b/services/item/common/models/item.json
@@ -79,6 +79,11 @@
"type": "hasMany",
"model": "ItemTag",
"foreignKey": "itemFk"
+ },
+ "itemBarcode": {
+ "type": "hasMany",
+ "model": "ItemBarcode",
+ "foreignKey": "itemFk"
}
}
}
\ No newline at end of file
diff --git a/services/item/common/models/itemBarcode.js b/services/item/common/models/itemBarcode.js
new file mode 100644
index 000000000..be746ecb3
--- /dev/null
+++ b/services/item/common/models/itemBarcode.js
@@ -0,0 +1,3 @@
+module.exports = function(Self) {
+ require('../methods/item/crudItemBarcodes.js')(Self);
+};
diff --git a/services/item/common/models/itemBarcode.json b/services/item/common/models/itemBarcode.json
new file mode 100644
index 000000000..9274cae98
--- /dev/null
+++ b/services/item/common/models/itemBarcode.json
@@ -0,0 +1,28 @@
+{
+ "name": "ItemBarcode",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "itemBarcode",
+ "database": "vn"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "Number",
+ "id": true,
+ "description": "Identifier"
+ },
+ "code": {
+ "type": "String",
+ "required": true
+ }
+ },
+ "relations": {
+ "item": {
+ "type": "belongsTo",
+ "model": "Item",
+ "foreignKey": "itemFk"
+ }
+ }
+ }
diff --git a/services/item/server/model-config.json b/services/item/server/model-config.json
index 09eed1735..8c1e5d26b 100644
--- a/services/item/server/model-config.json
+++ b/services/item/server/model-config.json
@@ -37,5 +37,8 @@
},
"ItemLog": {
"dataSource": "vn"
+ },
+ "ItemBarcode": {
+ "dataSource": "vn"
}
}
diff --git a/services_tests.js b/services_tests.js
index 068f8c8f2..fe3c907da 100644
--- a/services_tests.js
+++ b/services_tests.js
@@ -19,6 +19,7 @@ jasmine.loadConfig({
spec_files: [
'auth/server/**/*[sS]pec.js',
'client/common/**/*[sS]pec.js',
+ 'item/common/**/*[sS]pec.js',
'loopback/common/**/*[sS]pec.js'
],
helpers: [