diff --git a/e2e/helpers/extensions.js b/e2e/helpers/extensions.js index 5e4741da8..e9f01833f 100644 --- a/e2e/helpers/extensions.js +++ b/e2e/helpers/extensions.js @@ -1,8 +1,6 @@ /* eslint no-invalid-this: "off" */ import {url as defaultURL} from './config'; -let currentUser; - let actions = { clickIfExists: async function(selector) { let exists; @@ -49,34 +47,17 @@ let actions = { }, login: async function(userName) { - if (currentUser !== userName) { - let userPanelOpen = await this.evaluate(() => { - return document.querySelector('.user-popover'); - }); - if (!userPanelOpen) - await this.clickIfExists('#user'); - await this.clickIfExists('#logout'); - - try { - await this.waitForURL('#!/login'); - } catch (e) { - await this.goto(`${defaultURL}/#!/login`); - } - - await this.doLogin(userName); - await this.wait(() => { - return document.location.hash === '#!/'; - }, {}); - await this.changeLanguageToEnglish(); - - currentUser = userName; - } else { - await this.wait(() => { - return document.location.hash === '#!/'; - }, {}); - await this.changeLanguageToEnglish(); - await this.waitToClick('a[ui-sref=home]'); + try { + await this.waitForURL('#!/login'); + } catch (e) { + await this.goto(`${defaultURL}/#!/login`); } + + await this.doLogin(userName); + await this.wait(() => { + return document.location.hash === '#!/'; + }, {}); + await this.changeLanguageToEnglish(); }, selectModule: async function(moduleName) { @@ -129,7 +110,7 @@ let actions = { let field = await this.evaluate(selector => { return document.querySelector(`${selector} input`).closest('.vn-field').$ctrl.field; }, selector); - if (field && field.lenght) { + if ((field != null && field != '') || field == '0') { let coords = await this.evaluate(selector => { let rect = document.querySelector(selector).getBoundingClientRect(); return {x: rect.x + (rect.width / 2), y: rect.y + (rect.height / 2), width: rect.width}; @@ -371,7 +352,7 @@ let actions = { await this.write(`.vn-drop-down.shown`, searchValue); await this.waitFor(100); // ul to repaint await this.waitToClick(`.vn-drop-down.shown li.active`); - await this.waitFor(100); // input to asign value + await this.waitFor(200); // input to asign value await this.wait((autocompleteSelector, searchValue) => { return document.querySelector(`${autocompleteSelector} input`).value .toLowerCase() @@ -454,6 +435,15 @@ let actions = { transition.addEventListener('transitionend', onEnd); }); }, selector); + }, + + waitForContentLoaded: async function() { + await this.evaluate(() => { + return new Promise(resolve => { + const $rootScope = angular.element(document.body).injector().get('$rootScope'); + $rootScope.$on('$viewContentLoaded', resolve()); + }); + }); } }; diff --git a/e2e/helpers/puppeteer.js b/e2e/helpers/puppeteer.js index 314568d36..cafbd7972 100644 --- a/e2e/helpers/puppeteer.js +++ b/e2e/helpers/puppeteer.js @@ -3,36 +3,24 @@ import Puppeteer from 'puppeteer'; import {extendPage} from './extensions'; import {url as defaultURL} from './config'; -let browser; - -export function getBrowser() { - return browser || {close: () => {}}; -} - -async function openPage(url = defaultURL) { - if (!browser) { - browser = await Puppeteer.launch({ - args: [ - '--start-maximized' - // '--start-fullscreen' - ], - defaultViewport: null, - headless: false, - slowMo: 0, // slow down by ms - }); - } - const page = await browser.newPage(); - await page.setDefaultTimeout(5000); - await page.goto(url, {waitUntil: 'networkidle0'}); - - page.on('console', msg => { - let type = msg.type(); - if (type === 'error') - console[type](msg.text()); +export async function getBrowser() { + const browser = await Puppeteer.launch({ + args: [ + // '--start-maximized' + // '--start-fullscreen' + // '--proxy-server="direct://"', + // '--proxy-bypass-list=*' + `--window-size=${ 1920 },${ 1080 }`, + ], + defaultViewport: null, + headless: false, + slowMo: 0, // slow down by ms }); - - const extendedPage = extendPage(page); - return extendedPage; + let page = (await browser.pages())[0]; + page = extendPage(page); + await page.setDefaultTimeout(5000); + await page.goto(defaultURL, {waitUntil: 'networkidle0'}); + return {page, close: browser.close.bind(browser)}; } -export default openPage; +export default getBrowser; diff --git a/e2e/paths/01-login/01_login.spec.js b/e2e/paths/01-login/01_login.spec.js index 8701d8bff..ff9752b2b 100644 --- a/e2e/paths/01-login/01_login.spec.js +++ b/e2e/paths/01-login/01_login.spec.js @@ -1,13 +1,15 @@ -import openPage from '../../helpers/puppeteer'; +import getBrowser from '../../helpers/puppeteer'; describe('Login path', async() => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; }); afterAll(async() => { - page.close(); + await browser.close(); }); it('should receive an error when the username is incorrect', async() => { diff --git a/e2e/paths/02-client-module/01_create_client.spec.js b/e2e/paths/02-client-module/01_create_client.spec.js index 739f45d77..a2bc5a252 100644 --- a/e2e/paths/02-client-module/01_create_client.spec.js +++ b/e2e/paths/02-client-module/01_create_client.spec.js @@ -1,15 +1,17 @@ import selectors from '../../helpers/selectors'; -import openPage from '../../helpers/puppeteer'; +import getBrowser from '../../helpers/puppeteer'; describe('Client create path', async() => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); }); afterAll(async() => { - page.close(); + await browser.close(); }); it(`should search for the user Carol Danvers to confirm it isn't created yet`, async() => { @@ -55,6 +57,7 @@ describe('Client create path', async() => { await page.autocompleteSearch(selectors.createClientView.province, 'Province one'); await page.write(selectors.createClientView.city, 'Valencia'); await page.write(selectors.createClientView.postcode, '46000'); + await page.clearInput(selectors.createClientView.email); await page.write(selectors.createClientView.email, 'incorrect email format'); await page.waitToClick(selectors.createClientView.createButton); diff --git a/e2e/paths/02-client-module/02_edit_basic_data.spec.js b/e2e/paths/02-client-module/02_edit_basic_data.spec.js index cc86c2773..94b617ed9 100644 --- a/e2e/paths/02-client-module/02_edit_basic_data.spec.js +++ b/e2e/paths/02-client-module/02_edit_basic_data.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client Edit basicData path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); await page.accessToSearchResult('Bruce Wayne'); await page.accessToSection('client.card.basicData'); }); afterAll(async() => { - page.close(); + await browser.close(); }); describe('as employee', () => { diff --git a/e2e/paths/02-client-module/03_edit_fiscal_data.spec.js b/e2e/paths/02-client-module/03_edit_fiscal_data.spec.js index 6eac80c43..25127e314 100644 --- a/e2e/paths/02-client-module/03_edit_fiscal_data.spec.js +++ b/e2e/paths/02-client-module/03_edit_fiscal_data.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client Edit fiscalData path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); await page.accessToSearchResult('Bruce Banner'); await page.accessToSection('client.card.address.index'); }); afterAll(async() => { - page.close(); + await browser.close(); }); describe('as employee', () => { diff --git a/e2e/paths/02-client-module/04_edit_billing_data.spec.js b/e2e/paths/02-client-module/04_edit_billing_data.spec.js index eac256db6..5d85eda21 100644 --- a/e2e/paths/02-client-module/04_edit_billing_data.spec.js +++ b/e2e/paths/02-client-module/04_edit_billing_data.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client Edit billing data path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('administrative', 'client'); await page.accessToSearchResult('Bruce Banner'); await page.accessToSection('client.card.billingData'); }); afterAll(async() => { - page.close(); + await browser.close(); }); it(`should attempt to edit the billing data without an IBAN but fail`, async() => { diff --git a/e2e/paths/02-client-module/05_add_address.spec.js b/e2e/paths/02-client-module/05_add_address.spec.js index fb7e8815d..8fd207be3 100644 --- a/e2e/paths/02-client-module/05_add_address.spec.js +++ b/e2e/paths/02-client-module/05_add_address.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client Add address path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); await page.accessToSearchResult('Bruce Banner'); await page.accessToSection('client.card.address.index'); }); afterAll(async() => { - page.close(); + browser.close(); }); it(`should click on the add new address button to access to the new address form`, async() => { @@ -24,11 +26,8 @@ describe('Client Add address path', () => { it('should receive an error after clicking save button as consignee, street and town fields are empty', async() => { await page.waitToClick(selectors.clientAddresses.defaultCheckboxInput); - await page.clearInput(selectors.clientAddresses.streetAddressInput); await page.autocompleteSearch(selectors.clientAddresses.provinceAutocomplete, 'Province one'); - await page.clearInput(selectors.clientAddresses.cityInput); await page.write(selectors.clientAddresses.cityInput, 'Valencia'); - await page.clearInput(selectors.clientAddresses.postcodeInput); await page.write(selectors.clientAddresses.postcodeInput, '46000'); await page.autocompleteSearch(selectors.clientAddresses.agencyAutocomplete, 'Entanglement'); await page.write(selectors.clientAddresses.phoneInput, '999887744'); @@ -39,25 +38,6 @@ describe('Client Add address path', () => { expect(result).toEqual('Some fields are invalid'); }); - it('should confirm the postcode have been edited', async() => { - const result = await page.waitToGetProperty(`${selectors.clientAddresses.postcodeInput} input`, 'value'); - - expect(result).toContain('46000'); - }); - - it('should confirm the city have been autocompleted', async() => { - const result = await page.waitToGetProperty(`${selectors.clientAddresses.cityInput} input`, 'value'); - - expect(result).toEqual('Valencia'); - }); - - - it(`should confirm the province have been autocompleted`, async() => { - const result = await page.waitToGetProperty(`${selectors.clientAddresses.provinceAutocomplete} input`, 'value'); - - expect(result).toEqual('Province one'); - }); - it(`should create a new address with all it's data`, async() => { await page.write(selectors.clientAddresses.consigneeInput, 'Bruce Bunner'); await page.write(selectors.clientAddresses.streetAddressInput, '320 Park Avenue New York'); @@ -67,14 +47,20 @@ describe('Client Add address path', () => { expect(result).toEqual('Data saved!'); }); - it(`should click on the addresses button confirm the new address exists and it's the default one`, async() => { + it(`should click on the first address button to confirm the new address exists and it's the default one`, async() => { const result = await page.waitToGetProperty(selectors.clientAddresses.defaultAddress, 'innerText'); expect(result).toContain('320 Park Avenue New York'); }); - it(`should click on the make default icon of the second address then confirm it is the default one now`, async() => { + it(`should click on the make default icon of the second address`, async() => { await page.waitToClick(selectors.clientAddresses.secondMakeDefaultStar); + const result = await page.waitForLastSnackbar(); + + expect(result).toEqual('Data saved!'); + }); + + it(`should confirm the default address is the expected one`, async() => { await page.waitForTextInElement(selectors.clientAddresses.defaultAddress, 'Somewhere in Thailand'); const result = await page.waitToGetProperty(selectors.clientAddresses.defaultAddress, 'innerText'); @@ -100,9 +86,11 @@ describe('Client Add address path', () => { }); it(`should go back to the addreses section by clicking the cancel button`, async() => { - await page.waitToClick(selectors.clientAddresses.cancelEditAddressButton); - await page.waitToClick('.vn-confirm.shown button[response="accept"]'); - await page.waitForURL('address/index'); + page.waitToClick(selectors.clientAddresses.cancelEditAddressButton); + await Promise.all([ + page.waitForNavigation({waitUntil: ['load', 'networkidle0', 'domcontentloaded']}), + page.waitToClick('.vn-confirm.shown button[response="accept"]') + ]); const url = await page.parsedUrl(); expect(url.hash).toContain('address/index'); diff --git a/e2e/paths/02-client-module/06_add_address_notes.spec.js b/e2e/paths/02-client-module/06_add_address_notes.spec.js index fd6d17f5e..e3e07debf 100644 --- a/e2e/paths/02-client-module/06_add_address_notes.spec.js +++ b/e2e/paths/02-client-module/06_add_address_notes.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client add address notes path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); await page.accessToSearchResult('Petter Parker'); await page.accessToSection('client.card.address.index'); }); afterAll(async() => { - page.close(); + browser.close(); }); it(`should click on the edit icon of the default address`, async() => { diff --git a/e2e/paths/02-client-module/07_edit_web_access.spec.js b/e2e/paths/02-client-module/07_edit_web_access.spec.js index 066eb3330..b7b9bd0aa 100644 --- a/e2e/paths/02-client-module/07_edit_web_access.spec.js +++ b/e2e/paths/02-client-module/07_edit_web_access.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client Edit web access path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); await page.accessToSearchResult('Bruce Banner'); await page.accessToSection('client.card.webAccess'); }); afterAll(async() => { - page.close(); + browser.close(); }); it(`should uncheck the Enable web access checkbox and update the name`, async() => { diff --git a/e2e/paths/02-client-module/08_add_notes.spec.js b/e2e/paths/02-client-module/08_add_notes.spec.js index b80cc0668..6628ab666 100644 --- a/e2e/paths/02-client-module/08_add_notes.spec.js +++ b/e2e/paths/02-client-module/08_add_notes.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client Add notes path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); await page.accessToSearchResult('Bruce Banner'); await page.accessToSection('client.card.note.index'); }); afterAll(async() => { - page.close(); + browser.close(); }); it(`should click on the add note button`, async() => { diff --git a/e2e/paths/02-client-module/09_add_credit.spec.js b/e2e/paths/02-client-module/09_add_credit.spec.js index 5b9e347ad..931aa9e17 100644 --- a/e2e/paths/02-client-module/09_add_credit.spec.js +++ b/e2e/paths/02-client-module/09_add_credit.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client Add credit path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('salesAssistant', 'client'); await page.accessToSearchResult('Hank Pym'); await page.accessToSection('client.card.credit.index'); }); afterAll(async() => { - page.close(); + browser.close(); }); it(`should click on the add credit button`, async() => { diff --git a/e2e/paths/02-client-module/10_add_greuge.spec.js b/e2e/paths/02-client-module/10_add_greuge.spec.js index 8240b1769..b1941dc77 100644 --- a/e2e/paths/02-client-module/10_add_greuge.spec.js +++ b/e2e/paths/02-client-module/10_add_greuge.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client Add greuge path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('salesAssistant', 'client'); await page.accessToSearchResult('Petter Parker'); await page.accessToSection('client.card.greuge.index'); }); afterAll(async() => { - page.close(); + browser.close(); }); it(`should click on the add greuge button`, async() => { diff --git a/e2e/paths/02-client-module/11_mandate.spec.js b/e2e/paths/02-client-module/11_mandate.spec.js index 5c394825f..2b1318b09 100644 --- a/e2e/paths/02-client-module/11_mandate.spec.js +++ b/e2e/paths/02-client-module/11_mandate.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client mandate path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('salesPerson', 'client'); await page.accessToSearchResult('Petter Parker'); await page.accessToSection('client.card.mandate'); }); afterAll(async() => { - page.close(); + browser.close(); }); it('should confirm the client has a mandate of the CORE type', async() => { diff --git a/e2e/paths/02-client-module/12_lock_of_verified_data.spec.js b/e2e/paths/02-client-module/12_lock_of_verified_data.spec.js index 7d5176076..46b6bdcce 100644 --- a/e2e/paths/02-client-module/12_lock_of_verified_data.spec.js +++ b/e2e/paths/02-client-module/12_lock_of_verified_data.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client lock verified data path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('salesPerson', 'client'); await page.accessToSearchResult('Hank Pym'); await page.accessToSection('client.card.fiscalData'); }); afterAll(async() => { - page.close(); + browser.close(); }); describe('as salesPerson', () => { diff --git a/e2e/paths/02-client-module/13_log.spec.js b/e2e/paths/02-client-module/13_log.spec.js index 125d20bbb..04050bd46 100644 --- a/e2e/paths/02-client-module/13_log.spec.js +++ b/e2e/paths/02-client-module/13_log.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client log path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); await page.accessToSearchResult('DavidCharlesHaller'); await page.accessToSection('client.card.basicData'); }); afterAll(async() => { - page.close(); + browser.close(); }); it('should update the clients name', async() => { diff --git a/e2e/paths/02-client-module/14_balance.spec.js b/e2e/paths/02-client-module/14_balance.spec.js index 019840e67..9223ad21b 100644 --- a/e2e/paths/02-client-module/14_balance.spec.js +++ b/e2e/paths/02-client-module/14_balance.spec.js @@ -1,16 +1,18 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client balance path', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('administrative', 'client'); await page.accessToSearchResult('Petter Parker'); }, 30000); afterAll(async() => { - page.close(); + browser.close(); }); it('should now edit the local user config data', async() => { diff --git a/e2e/paths/02-client-module/15_user_config.spec.js b/e2e/paths/02-client-module/15_user_config.spec.js index 56f93f540..cab5f2dc4 100644 --- a/e2e/paths/02-client-module/15_user_config.spec.js +++ b/e2e/paths/02-client-module/15_user_config.spec.js @@ -1,14 +1,16 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('User config', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; }); afterAll(async() => { - page.close(); + browser.close(); }); describe('as salesPerson', () => { diff --git a/e2e/paths/02-client-module/16_web_payment.spec.js b/e2e/paths/02-client-module/16_web_payment.spec.js index 846d86a9a..85fe169a1 100644 --- a/e2e/paths/02-client-module/16_web_payment.spec.js +++ b/e2e/paths/02-client-module/16_web_payment.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client web Payment', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('employee', 'client'); await page.accessToSearchResult('Tony Stark'); await page.accessToSection('client.card.webPayment'); }); afterAll(async() => { - page.close(); + browser.close(); }); describe('as employee', () => { diff --git a/e2e/paths/02-client-module/17_dms.spec.js b/e2e/paths/02-client-module/17_dms.spec.js index f5dbba55c..5e7b182c1 100644 --- a/e2e/paths/02-client-module/17_dms.spec.js +++ b/e2e/paths/02-client-module/17_dms.spec.js @@ -1,17 +1,19 @@ -import selectors from '../../helpers/selectors.js'; -import openPage from '../../helpers/puppeteer'; +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; describe('Client DMS', () => { + let browser; let page; beforeAll(async() => { - page = await openPage(); + browser = await getBrowser(); + page = browser.page; await page.loginAndModule('salesPerson', 'client'); await page.accessToSearchResult('Tony Stark'); await page.accessToSection('client.card.dms.index'); }); afterAll(async() => { - page.close(); + browser.close(); }); describe('as salesPerson', () => {