diff --git a/Jenkinsfile b/Jenkinsfile index 1fa6a49ea4..9af09b3060 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,7 +68,7 @@ pipeline { stage('Backend') { steps { nodejs('node-lts') { - sh 'gulp backTestDockerOnce --junit --random' + sh 'gulp backTestOnce --ci' } } } diff --git a/README.md b/README.md index 52f854b6e9..82f7497659 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,12 @@ Salix is also the scientific name of a beautifull tree! :) Required applications. -* Visual Studio Code * Node.js = 12.17.0 LTS * Docker -In Visual Studio Code we use the ESLint extension. Open Visual Studio Code, press Ctrl+P and paste the following command. -``` -ext install dbaeumer.vscode-eslint -``` - You will need to install globally the following items. ``` -# sudo npm install -g jest gulp-cli nodemon +# sudo npm install -g jest gulp-cli ``` ## Linux Only Prerequisites @@ -65,6 +59,15 @@ For end-to-end tests run from project's root. $ gulp e2e ``` +## Recommended tools + +* Visual Studio Code + +In Visual Studio Code we use the ESLint extension. Open Visual Studio Code, press Ctrl+P and paste the following command. +``` +ext install dbaeumer.vscode-eslint +``` + ## Built With * [angularjs](https://angularjs.org/) @@ -75,4 +78,4 @@ $ gulp e2e * [gulp.js](https://gulpjs.com/) * [jest](https://jestjs.io/) * [Jasmine](https://jasmine.github.io/) -* [Nightmare](http://www.nightmarejs.org/) +* [Puppeteer](https://pptr.dev/) diff --git a/back/models/account.js b/back/models/account.js index 7cc1968523..a0b08dd57a 100644 --- a/back/models/account.js +++ b/back/models/account.js @@ -15,20 +15,6 @@ module.exports = Self => { Self.observe('before save', async function(ctx) { if (ctx.currentInstance && ctx.currentInstance.id && ctx.data && ctx.data.password) ctx.data.password = md5(ctx.data.password); - - if (!ctx.isNewInstance && ctx.data && (ctx.data.name || ctx.data.active)) { - let instance = JSON.parse(JSON.stringify(ctx.currentInstance)); - let userId = ctx.options.accessToken.userId; - let logRecord = { - originFk: ctx.currentInstance.id, - userFk: userId, - action: 'update', - changedModel: 'Account', - oldInstance: {name: instance.name, active: instance.active}, - newInstance: ctx.data - }; - await Self.app.models.ClientLog.create(logRecord); - } }); Self.remoteMethod('getCurrentUserData', { diff --git a/db/docker.js b/db/docker.js new file mode 100644 index 0000000000..849cf182cc --- /dev/null +++ b/db/docker.js @@ -0,0 +1,166 @@ +const exec = require('child_process').exec; +const log = require('fancy-log'); +const dataSources = require('../loopback/server/datasources.json'); + +module.exports = class Docker { + constructor(name) { + Object.assign(this, { + id: name, + name, + isRandom: name == null, + dbConf: Object.assign({}, dataSources.vn) + }); + } + + /** + * Builds the database image and runs a container. It only rebuilds the + * image when fixtures have been modified or when the day on which the + * image was built is different to today. Some workarounds have been used + * to avoid a bug with OverlayFS driver on MacOS. + * + * @param {Boolean} ci continuous integration environment argument + */ + async run(ci) { + let d = new Date(); + let pad = v => v < 10 ? '0' + v : v; + let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + await this.execP(`docker build --build-arg STAMP=${stamp} -t salix-db ./db`); + + let dockerArgs; + + if (this.isRandom) + dockerArgs = '-p 3306'; + else { + try { + await this.rm(); + } catch (e) {} + dockerArgs = `--name ${this.name} -p 3306:${this.dbConf.port}`; + } + + let runChown = process.platform != 'linux'; + + let container = await this.execP(`docker run --env RUN_CHOWN=${runChown} -d ${dockerArgs} salix-db`); + this.id = container.stdout; + + try { + if (this.isRandom) { + let inspect = await this.execP(`docker inspect -f "{{json .NetworkSettings}}" ${this.id}`); + let netSettings = JSON.parse(inspect.stdout); + + if (ci) + this.dbConf.host = netSettings.Gateway; + + this.dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort']; + } + + if (runChown) await this.wait(); + } catch (err) { + if (this.isRandom) + await this.rm(); + throw err; + } + } + + /** + * Does the minium effort to start the database container, if it doesn't exists + * calls the 'docker' task, if it is started does nothing. Keep in mind that when + * you do not rebuild the docker you may be using an outdated version of it. + * See the 'docker' task for more info. + */ + async start() { + let state; + try { + let result = await this.execP(`docker inspect -f "{{json .State}}" ${this.id}`); + state = JSON.parse(result.stdout); + } catch (err) { + return await this.run(); + } + + switch (state.Status) { + case 'running': + return; + case 'exited': + await this.execP(`docker start ${this.id}`); + await this.wait(); + return; + default: + throw new Error(`Unknown docker status: ${state.Status}`); + } + } + + wait() { + return new Promise((resolve, reject) => { + const mysql = require('mysql2'); + + let interval = 100; + let elapsedTime = 0; + let maxInterval = 4 * 60 * 1000; + + let myConf = { + user: this.dbConf.username, + password: this.dbConf.password, + host: this.dbConf.host, + port: this.dbConf.port + }; + + log('Waiting for MySQL init process...'); + + async function checker() { + elapsedTime += interval; + let state; + + try { + let result = await this.execP(`docker container inspect -f "{{json .State}}" ${this.id}`); + state = JSON.parse(result.stdout); + } catch (err) { + return reject(new Error(err.message)); + } + + if (state.Status === 'exited') + return reject(new Error('Docker exited, please see the docker logs for more info')); + + let conn = mysql.createConnection(myConf); + conn.on('error', () => {}); + conn.connect(err => { + conn.destroy(); + if (!err) { + log('MySQL process ready.'); + return resolve(); + } + + if (elapsedTime >= maxInterval) + reject(new Error(`MySQL not initialized whithin ${elapsedTime / 1000} secs`)); + else + setTimeout(bindedChecker, interval); + }); + } + let bindedChecker = checker.bind(this); + bindedChecker(); + }); + } + + rm() { + return this.execP(`docker rm -fv ${this.id}`); + } + + /** + * Promisified version of exec(). + * + * @param {String} command The exec command + * @return {Promise} The promise + */ + execP(command) { + return new Promise((resolve, reject) => { + exec(command, (err, stdout, stderr) => { + if (err) + reject(err); + else { + resolve({ + stdout: stdout, + stderr: stderr + }); + } + }); + }); + } +}; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 13ac2bcb3c..e48a20ec65 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -664,12 +664,12 @@ INSERT INTO `vn`.`itemCategory`(`id`, `name`, `display`, `color`, `icon`, `code` INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `life`,`workerFk`, `isPackaging`) VALUES - (1, 'CRI', 'Crisantemo', 2, 31, 5, 0), - (2, 'ITG', 'Anthurium', 1, 31, 5, 0), - (3, 'WPN', 'Paniculata', 2, 31, 5, 0), - (4, 'PRT', 'Delivery ports', 3, NULL, 5, 1), - (5, 'CON', 'Container', 3, NULL, 5, 1), - (6, 'ALS', 'Alstroemeria', 1, 31, 5, 0); + (1, 'CRI', 'Crisantemo', 2, 31, 35, 0), + (2, 'ITG', 'Anthurium', 1, 31, 35, 0), + (3, 'WPN', 'Paniculata', 2, 31, 35, 0), + (4, 'PRT', 'Delivery ports', 3, NULL, 35, 1), + (5, 'CON', 'Container', 3, NULL, 35, 1), + (6, 'ALS', 'Alstroemeria', 1, 31, 35, 0); INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`) VALUES diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 5cd24cda5f..98533b8c35 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -10,7 +10,7 @@ export default { ticketsButton: '.modules-menu [ui-sref="ticket.index"]', invoiceOutButton: '.modules-menu [ui-sref="invoiceOut.index"]', claimsButton: '.modules-menu [ui-sref="claim.index"]', - returnToModuleIndexButton: 'a[ui-sref="order.index"]', + returnToModuleIndexButton: 'a[name="goToModuleIndex"]', homeButton: 'vn-topbar > div.side.start > a', userLocalWarehouse: '.user-popover vn-autocomplete[ng-model="$ctrl.localWarehouseFk"]', userLocalBank: '.user-popover vn-autocomplete[ng-model="$ctrl.localBankFk"]', @@ -365,7 +365,8 @@ export default { firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(3)', firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(6)', invoiceOutRef: 'vn-ticket-summary > vn-card > vn-horizontal > vn-one:nth-child(1) > vn-label-value:nth-child(7) > section > span', - setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button' + setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button', + descriptorTicketId: 'vn-ticket-descriptor > vn-descriptor-content > div > div.body > div.top > div' }, ticketsIndex: { openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]', diff --git a/e2e/paths/05-ticket/14_create_ticket.spec.js b/e2e/paths/05-ticket/14_create_ticket.spec.js index 4ce2e51565..26c22ad3df 100644 --- a/e2e/paths/05-ticket/14_create_ticket.spec.js +++ b/e2e/paths/05-ticket/14_create_ticket.spec.js @@ -4,6 +4,9 @@ import getBrowser from '../../helpers/puppeteer'; describe('Ticket create path', () => { let browser; let page; + let nextMonth = new Date(); + nextMonth.setMonth(nextMonth.getMonth() + 1); + let stowawayTicketId; beforeAll(async() => { browser = await getBrowser(); @@ -21,13 +24,9 @@ describe('Ticket create path', () => { }); it('should succeed to create a ticket', async() => { - const nextMonth = new Date(); - nextMonth.setMonth(nextMonth.getMonth() + 1); - - await page.autocompleteSearch(selectors.createTicketView.client, 'Tony Stark'); - await page.autocompleteSearch(selectors.createTicketView.address, 'Tony Stark'); + await page.autocompleteSearch(selectors.createTicketView.client, 'Clark Kent'); await page.pickDate(selectors.createTicketView.deliveryDate, nextMonth); - await page.autocompleteSearch(selectors.createTicketView.warehouse, 'Warehouse One'); + await page.autocompleteSearch(selectors.createTicketView.warehouse, 'Warehouse Two'); await page.autocompleteSearch(selectors.createTicketView.agency, 'Silla247'); await page.waitToClick(selectors.createTicketView.createButton); const message = await page.waitForSnackbar(); @@ -37,5 +36,53 @@ describe('Ticket create path', () => { it('should check the url is now the summary of the ticket', async() => { await page.waitForState('ticket.card.summary'); + stowawayTicketId = await page.waitToGetProperty(selectors.ticketSummary.descriptorTicketId, 'innerText'); + stowawayTicketId = stowawayTicketId.substring(1); + }); + + it('should again open the new ticket form', async() => { + await page.waitToClick(selectors.globalItems.returnToModuleIndexButton); + await page.waitToClick(selectors.ticketsIndex.newTicketButton); + await page.waitForState('ticket.create'); + }); + + it('should succeed to create another ticket for the same client', async() => { + await page.autocompleteSearch(selectors.createTicketView.client, 'Clark Kent'); + await page.pickDate(selectors.createTicketView.deliveryDate, nextMonth); + await page.autocompleteSearch(selectors.createTicketView.warehouse, 'Warehouse One'); + await page.autocompleteSearch(selectors.createTicketView.agency, 'Silla247'); + await page.waitToClick(selectors.createTicketView.createButton); + const message = await page.waitForSnackbar(); + + expect(message.type).toBe('success'); + }); + + it('should check the url is now the summary of the created ticket', async() => { + await page.waitForState('ticket.card.summary'); + }); + + it('should make the previously created ticket the stowaway of the current ticket', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuAddStowaway); + await page.waitToClick(selectors.ticketDescriptor.addStowawayDialogFirstTicket); + const message = await page.waitForSnackbar(); + + expect(message.type).toBe('success'); + }); + + it('should delete the current ticket', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket); + await page.waitToClick(selectors.ticketDescriptor.acceptDeleteButton); + const message = await page.waitForSnackbar(); + + expect(message.type).toBe('success'); + }); + + it('should search for the stowaway ticket of the previously deleted ticket', async() => { + await page.accessToSearchResult(stowawayTicketId); + const result = await page.countElement(selectors.ticketDescriptor.shipButton); + + expect(result).toBe(0); }); }); diff --git a/e2e/smokes-tests.js b/e2e/smokes-tests.js deleted file mode 100644 index 7b4e16edfb..0000000000 --- a/e2e/smokes-tests.js +++ /dev/null @@ -1,35 +0,0 @@ -require('babel-core/register')({presets: ['es2015']}); - -process.on('warning', warning => { - console.log(warning.name); - console.log(warning.message); - console.log(warning.stack); -}); - -let verbose = false; - -if (process.argv[2] === '--v') - verbose = true; - -let Jasmine = require('jasmine'); -let jasmine = new Jasmine(); -let SpecReporter = require('jasmine-spec-reporter').SpecReporter; - -jasmine.loadConfig({ - spec_files: [ - `${__dirname}/smokes/**/*[sS]pec.js`, - `${__dirname}/helpers/extensions.js` - ], - helpers: [] -}); - -jasmine.addReporter(new SpecReporter({ - spec: { - // displayStacktrace: 'summary', - displaySuccessful: verbose, - displayFailedSpec: true, - displaySpecDuration: true - } -})); - -jasmine.execute(); diff --git a/e2e/smokes/01_client_path.spec.js b/e2e/smokes/01_client_path.spec.js deleted file mode 100644 index 6c106b2eb2..0000000000 --- a/e2e/smokes/01_client_path.spec.js +++ /dev/null @@ -1,31 +0,0 @@ -import selectors from '../../helpers/selectors.js'; -import getBrowser from '../../helpers/puppeteer'; - -describe('create client path', () => { - let browser; - let page; - beforeAll(async() => { - browser = await getBrowser(); - page = browser.page; - await page.loginAndModule('employee', 'client'); - }); - - afterAll(async() => { - await browser.close(); - }); - - it('should access to the create client view by clicking the create-client floating button', async() => { - await page.waitToClick(selectors.clientsIndex.createClientButton); - let url = await page.expectURL('#!/client/create'); - - expect(url).toBe(true); - }); - - it('should cancel the client creation to go back to clients index', async() => { - await page.waitToClick(selectors.globalItems.applicationsMenuButton); - await page.waitToClick(selectors.globalItems.clientsButton); - let url = await page.expectURL('#!/client/index'); - - expect(url).toBe(true); - }); -}); diff --git a/front/core/components/searchbar/searchbar.html b/front/core/components/searchbar/searchbar.html index 1de40fa231..b41c050ebe 100644 --- a/front/core/components/searchbar/searchbar.html +++ b/front/core/components/searchbar/searchbar.html @@ -1,7 +1,7 @@