diff --git a/package.json b/package.json index 7d0928820..f3c1f648a 100644 --- a/package.json +++ b/package.json @@ -224,6 +224,7 @@ "copy-webpack-plugin": "^6.0.3", "cross-spawn": "^7.0.3", "css-loader": "^2.1.1", + "css-to-xpath": "^0.1.0", "del": "^3.0.0", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.15.1", diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 6aa7cc268..358c41161 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -1,5 +1,5 @@ const assert = require('assert'); -const { Key, until } = require('selenium-webdriver'); +const { Key } = require('selenium-webdriver'); const enLocaleMessages = require('../../app/_locales/en/messages.json'); const { tinyDelayMs, regularDelayMs, largeDelayMs } = require('./helpers'); @@ -143,10 +143,12 @@ describe('MetaMask', function () { await driver.findVisibleElement('.qr-code__wrapper'); await driver.delay(regularDelayMs); - const accountModal = await driver.findElement('span .modal'); + // wait for permission modal to be visible. + await driver.waitForSelector('span .modal'); await driver.clickElement('.account-modal__close'); - await driver.wait(until.stalenessOf(accountModal)); + // wait for account modal to be removed from DOM. + await driver.waitForSelector('span .modal', { state: 'detached' }); await driver.delay(regularDelayMs); }); }); @@ -242,10 +244,10 @@ describe('MetaMask', function () { }); it('balance renders', async function () { - const balance = await driver.findElement( - '[data-testid="wallet-balance"] .list-item__heading', - ); - await driver.wait(until.elementTextMatches(balance, /100\s*ETH/u)); + await driver.waitForSelector({ + css: '[data-testid="wallet-balance"] .list-item__heading', + text: '100 ETH', + }); await driver.delay(regularDelayMs); }); }); @@ -317,10 +319,10 @@ describe('MetaMask', function () { return confirmedTxes.length === 1; }, 10000); - const txValues = await driver.findElement( - '.transaction-list-item__primary-currency', - ); - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/u), 10000); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }); }); }); @@ -355,17 +357,13 @@ describe('MetaMask', function () { }); it('finds the transaction in the transactions list', async function () { - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 2; - }, 10000); - - const txValues = await driver.findElement( - '.transaction-list-item__primary-currency', + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-child(2)', ); - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/u), 10000); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }); }); }); @@ -389,9 +387,12 @@ describe('MetaMask', function () { await driver.clickElement('.advanced-gas-options-btn'); await driver.delay(regularDelayMs); - const gasModal = await driver.findElement('span .modal'); await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.wait(until.stalenessOf(gasModal)); + + // Wait for gas modal to be removed from DOM + await driver.waitForSelector('span .modal', { + state: 'detached', + }); await driver.delay(regularDelayMs); // Continue to next screen @@ -418,10 +419,13 @@ describe('MetaMask', function () { return confirmedTxes.length === 3; }, 10000); - const txValues = await driver.findElement( - '.transaction-list-item__primary-currency', + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }, + { timeout: 10000 }, ); - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/u), 10000); }); }); @@ -542,10 +546,10 @@ describe('MetaMask', function () { return confirmedTxes.length === 4; }, 10000); - const txValue = await driver.findClickableElement( - '.transaction-list-item__primary-currency', - ); - await driver.wait(until.elementTextMatches(txValue, /-3\s*ETH/u), 10000); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-3 ETH', + }); }); it('the transaction has the expected gas price', async function () { @@ -556,10 +560,10 @@ describe('MetaMask', function () { const popoverCloseButton = await driver.findClickableElement( '.popover-header__button', ); - const txGasPrice = await driver.findElement( - '[data-testid="transaction-breakdown__gas-price"]', - ); - await driver.wait(until.elementTextMatches(txGasPrice, /^10$/u), 10000); + await driver.waitForSelector({ + css: '[data-testid="transaction-breakdown__gas-price"]', + text: '10', + }); await popoverCloseButton.click(); }); }); @@ -804,17 +808,16 @@ describe('MetaMask', function () { await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(largeDelayMs); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 6; - }, 10000); + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(6)', + ); - const txAction = await driver.findElements('.list-item__heading'); - await driver.wait( - until.elementTextMatches(txAction[0], /Contract\sDeployment/u), - 10000, + await driver.waitForSelector( + { + css: '.list-item__title', + text: 'Contract Deployment', + }, + { timeout: 10000 }, ); await driver.delay(regularDelayMs); }); @@ -823,19 +826,23 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(regularDelayMs); - let contractStatus = await driver.findElement('#contractStatus'); - await driver.wait( - until.elementTextMatches(contractStatus, /Deployed/u), - 15000, + await driver.waitForSelector( + { + css: '#contractStatus', + text: 'Deployed', + }, + { timeout: 15000 }, ); await driver.clickElement('#depositButton'); await driver.delay(largeDelayMs); - contractStatus = await driver.findElement('#contractStatus'); - await driver.wait( - until.elementTextMatches(contractStatus, /Deposit\sinitiated/u), - 10000, + await driver.waitForSelector( + { + css: '#contractStatus', + text: 'Deposit initiated', + }, + { timeout: 10000 }, ); await driver.switchToWindow(extension); @@ -845,19 +852,20 @@ describe('MetaMask', function () { const txListValue = await driver.findClickableElement( '.transaction-list-item__primary-currency', ); - await driver.wait( - until.elementTextMatches(txListValue, /-4\s*ETH/u), - 10000, + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-4 ETH', + }, + { timeout: 10000 }, ); await txListValue.click(); await driver.delay(regularDelayMs); // Set the gas limit await driver.clickElement('.confirm-detail-row__header-text--edit'); - await driver.delay(regularDelayMs); - - const gasModal = await driver.findElement('span .modal'); - await driver.delay(regularDelayMs); + // wait for gas modal to be detached from DOM + await driver.waitForSelector('span .modal'); await driver.clickElement('.page-container__tab:nth-of-type(2)'); await driver.delay(regularDelayMs); @@ -879,26 +887,24 @@ describe('MetaMask', function () { await driver.delay(1000); await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.delay(regularDelayMs); - await driver.wait(until.stalenessOf(gasModal)); + // wait for gas modal to be detached from DOM + await driver.waitForSelector('span .modal', { state: 'detached' }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 7; - }, 10000); - - const txValues = await driver.findElements( - '.transaction-list-item__primary-currency', + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(7)', + { timeout: 10000 }, ); - await driver.wait( - until.elementTextMatches(txValues[0], /-4\s*ETH/u), - 10000, + await driver.waitForSelector( + { + css: + '.transaction-list__completed-transactions .transaction-list-item__primary-currency', + text: '-4 ETH', + }, + { timeout: 10000 }, ); }); @@ -927,23 +933,25 @@ describe('MetaMask', function () { return confirmedTxes.length === 8; }, 10000); - const txValues = await driver.findElement( - '.transaction-list-item__primary-currency', + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-0 ETH', + }, + { timeout: 10000 }, ); - await driver.wait(until.elementTextMatches(txValues, /-0\s*ETH/u), 10000); await driver.closeAllWindowHandlesExcept([extension, dapp]); await driver.switchToWindow(extension); }); it('renders the correct ETH balance', async function () { - const balance = await driver.findElement( - '[data-testid="eth-overview__primary-currency"]', - ); - await driver.delay(regularDelayMs); - await driver.wait( - until.elementTextMatches(balance, /^87.*\s*ETH.*$/u), - 10000, + const balance = await driver.waitForSelector( + { + css: '[data-testid="eth-overview__primary-currency"]', + text: '87.', + }, + { timeout: 10000 }, ); const tokenAmount = await balance.getText(); assert.ok(/^87.*\s*ETH.*$/u.test(tokenAmount)); @@ -989,8 +997,10 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(tinyDelayMs); - const tokenContractAddress = await driver.findElement('#tokenAddress'); - await driver.wait(until.elementTextMatches(tokenContractAddress, /0x/u)); + const tokenContractAddress = await driver.waitForSelector({ + css: '#tokenAddress', + text: '0x', + }); tokenAddress = await tokenContractAddress.getText(); await driver.delay(regularDelayMs); @@ -1025,18 +1035,15 @@ describe('MetaMask', function () { }); it('renders the balance for the new token', async function () { - const balance = await driver.findElement( - '.wallet-overview .token-overview__primary-balance', - ); - await driver.wait(until.elementTextMatches(balance, /^10\s*TST\s*$/u)); - const tokenAmount = await balance.getText(); - assert.ok(/^10\s*TST\s*$/u.test(tokenAmount)); + await driver.waitForSelector({ + css: '.wallet-overview .token-overview__primary-balance', + text: '10 TST', + }); await driver.delay(regularDelayMs); }); }); describe('Send token from inside MetaMask', function () { - let gasModal; it('starts to send a transaction', async function () { await driver.clickElement('[data-testid="eth-overview-send"]'); await driver.delay(regularDelayMs); @@ -1048,24 +1055,20 @@ describe('MetaMask', function () { const inputAmount = await driver.findElement('.unit-input__input'); await inputAmount.sendKeys('1'); + }); - // Set the gas limit + it('opens customize gas modal and saves options to continue', async function () { await driver.clickElement('.advanced-gas-options-btn'); - await driver.delay(regularDelayMs); - gasModal = await driver.findElement('span .modal'); - await driver.delay(regularDelayMs); - }); - - it('opens customize gas modal', async function () { + // Wait for gas modal to be visible + await driver.waitForSelector('span .modal'); await driver.findElement('.page-container__title'); await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.delay(regularDelayMs); + // wait for gas modal to be removed from DOM. + await driver.waitForSelector('span .modal', { state: 'detached' }); }); it('transitions to the confirm screen', async function () { - await driver.wait(until.stalenessOf(gasModal)); - // Continue to next screen await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.delay(regularDelayMs); @@ -1116,25 +1119,22 @@ describe('MetaMask', function () { return confirmedTxes.length === 1; }, 10000); - const txValues = await driver.findElements( - '.transaction-list-item__primary-currency', - ); - assert.equal(txValues.length, 1); - await driver.wait( - until.elementTextMatches(txValues[0], /-1\s*TST/u), - 10000, + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-1 TST', + }, + { timeout: 10000 }, ); - const txStatuses = await driver.findElements('.list-item__heading'); - await driver.wait( - until.elementTextMatches(txStatuses[0], /Send\sTST/u), - 10000, - ); + await driver.waitForSelector({ + css: '.list-item__heading', + text: 'Send TST', + }); }); }); describe('Send a custom token from dapp', function () { - let gasModal; it('sends an already created token', async function () { const windowHandles = await driver.getAllWindowHandles(); const extension = windowHandles[0]; @@ -1153,14 +1153,14 @@ describe('MetaMask', function () { await driver.delay(largeDelayMs); await driver.findElements('.transaction-list__pending-transactions'); - const txListValue = await driver.findClickableElement( - '.transaction-list-item__primary-currency', + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }, + { timeout: 10000 }, ); - await driver.wait( - until.elementTextMatches(txListValue, /-1.5\s*TST/u), - 10000, - ); - await txListValue.click(); + await driver.clickElement('.transaction-list-item__primary-currency'); await driver.delay(regularDelayMs); const transactionAmounts = await driver.findElements( @@ -1173,7 +1173,8 @@ describe('MetaMask', function () { await driver.clickElement('.confirm-detail-row__header-text--edit'); await driver.delay(regularDelayMs); - gasModal = await driver.findElement('span .modal'); + // wait for gas modal to be visible + await driver.waitForSelector('span .modal'); }); it('customizes gas', async function () { @@ -1196,7 +1197,11 @@ describe('MetaMask', function () { await driver.delay(1000); await driver.clickElement('.page-container__footer-button'); - await driver.wait(until.stalenessOf(gasModal)); + + // Wait for gas modal to be removed from DOM. + await driver.waitForSelector('span .modal', { + state: 'detached', + }); const gasFeeInputs = await driver.findElements( '.confirm-detail-row__primary', @@ -1224,28 +1229,24 @@ describe('MetaMask', function () { return confirmedTxes.length === 2; }, 10000); - const txValues = await driver.findElements( - '.transaction-list-item__primary-currency', - ); - await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/u)); - const txStatuses = await driver.findElements('.list-item__heading'); - await driver.wait( - until.elementTextMatches(txStatuses[0], /Send\sTST/u), - 10000, - ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }); - const tokenBalanceAmount = await driver.findElements( - '.token-overview__primary-balance', - ); - await driver.wait( - until.elementTextMatches(tokenBalanceAmount[0], /7.5\s*TST/u), - 10000, - ); + await driver.waitForSelector({ + css: '.list-item__heading', + text: 'Send TST', + }); + + await driver.waitForSelector({ + css: '.token-overview__primary-balance', + text: '7.5 TST', + }); }); }); describe('Approves a custom token from dapp', function () { - let gasModal; it('approves an already created token', async function () { const windowHandles = await driver.getAllWindowHandles(); const extension = windowHandles[0]; @@ -1271,12 +1272,13 @@ describe('MetaMask', function () { return pendingTxes.length === 1; }, 10000); - const [txtListHeading] = await driver.findElements( - '.transaction-list-item .list-item__heading', - ); - await driver.wait( - until.elementTextMatches(txtListHeading, /Approve TST spend limit/u), - ); + await driver.waitForSelector({ + // Selects only the very first transaction list item immediately following the 'Pending' header + css: + '.transaction-list__pending-transactions .transaction-list__header + .transaction-list-item .list-item__heading', + text: 'Approve TST spend limit', + }); + await driver.clickElement('.transaction-list-item'); await driver.delay(regularDelayMs); }); @@ -1310,7 +1312,8 @@ describe('MetaMask', function () { ); await driver.delay(regularDelayMs); - gasModal = await driver.findElement('span .modal'); + // Wait for the gas modal to be visible + await driver.waitForSelector('span .modal'); }); it('customizes gas', async function () { @@ -1333,7 +1336,11 @@ describe('MetaMask', function () { await driver.delay(1000); await driver.clickElement('.page-container__footer-button'); - await driver.wait(until.stalenessOf(gasModal)); + + // wait for the gas modal to be removed from DOM. + await driver.waitForSelector('span .modal', { + state: 'detached', + }); const gasFeeInEth = await driver.findElement( '.confirm-approve-content__transaction-details-content__secondary-fee', @@ -1346,10 +1353,9 @@ describe('MetaMask', function () { '.confirm-approve-content__small-blue-text.cursor-pointer', ); await editButtons[1].click(); - await driver.delay(regularDelayMs); - - const permissionModal = await driver.findElement('span .modal'); + // wait for permission modal to be visible + await driver.waitForSelector('span .modal'); const radioButtons = await driver.findClickableElements( '.edit-approval-permission__edit-section__radio-button', ); @@ -1361,9 +1367,9 @@ describe('MetaMask', function () { await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.delay(regularDelayMs); - await driver.wait(until.stalenessOf(permissionModal)); + // wait for permission modal to be removed from DOM. + await driver.waitForSelector('span .modal', { state: 'detached' }); const permissionInfo = await driver.findElements( '.confirm-approve-content__medium-text', @@ -1385,10 +1391,12 @@ describe('MetaMask', function () { return confirmedTxes.length === 3; }, 10000); - const txStatuses = await driver.findElements('.list-item__heading'); - await driver.wait( - until.elementTextMatches(txStatuses[0], /Approve TST spend limit/u), - ); + await driver.waitForSelector({ + // Select only the heading of the first entry in the transaction list. + css: + '.transaction-list__completed-transactions .transaction-list-item:first-child .list-item__heading', + text: 'Approve TST spend limit', + }); }); }); @@ -1420,10 +1428,10 @@ describe('MetaMask', function () { return pendingTxes.length === 1; }, 10000); - const [txListValue] = await driver.findElements( - '.transaction-list-item__primary-currency', - ); - await driver.wait(until.elementTextMatches(txListValue, /-1.5\s*TST/u)); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }); await driver.clickElement('.transaction-list-item'); await driver.delay(regularDelayMs); }); @@ -1442,12 +1450,18 @@ describe('MetaMask', function () { return confirmedTxes.length === 4; }, 10000); - const txValues = await driver.findElements( - '.transaction-list-item__primary-currency', - ); - await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/u)); - const txStatuses = await driver.findElements('.list-item__heading'); - await driver.wait(until.elementTextMatches(txStatuses[0], /Send TST/u)); + await driver.waitForSelector({ + // Select the heading of the first transaction list item in the + // completed transaction list with text matching Send TST + css: + '.transaction-list__completed-transactions .transaction-list-item:first-child .list-item__heading', + text: 'Send TST', + }); + + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }); }); }); @@ -1480,12 +1494,13 @@ describe('MetaMask', function () { return pendingTxes.length === 1; }, 10000); - const [txtListHeading] = await driver.findElements( - '.transaction-list-item .list-item__heading', - ); - await driver.wait( - until.elementTextMatches(txtListHeading, /Approve TST spend limit/u), - ); + await driver.waitForSelector({ + // Selects only the very first transaction list item immediately following the 'Pending' header + css: + '.transaction-list__pending-transactions .transaction-list__header + .transaction-list-item .list-item__heading', + text: 'Approve TST spend limit', + }); + await driver.clickElement('.transaction-list-item'); await driver.delay(regularDelayMs); }); @@ -1517,10 +1532,11 @@ describe('MetaMask', function () { return confirmedTxes.length === 5; }, 10000); - const txStatuses = await driver.findElements('.list-item__heading'); - await driver.wait( - until.elementTextMatches(txStatuses[0], /Approve TST spend limit/u), - ); + await driver.waitForSelector({ + css: + '.transaction-list__completed-transactions .transaction-list-item:first-child .list-item__heading', + text: 'Approve TST spend limit', + }); }); }); @@ -1530,13 +1546,15 @@ describe('MetaMask', function () { await driver.clickElement('[data-testid="token-options__hide"]'); - const confirmHideModal = await driver.findElement('span .modal'); + // wait for confirm hide modal to be visible + await driver.waitForSelector('span .modal'); await driver.clickElement( '[data-testid="hide-token-confirmation__hide"]', ); - await driver.wait(until.stalenessOf(confirmHideModal)); + // wait for confirm hide modal to be removed from DOM. + await driver.waitForSelector('span .modal', { state: 'detached' }); }); }); @@ -1562,10 +1580,10 @@ describe('MetaMask', function () { }); it('renders the balance for the chosen token', async function () { - const balance = await driver.findElement( - '.token-overview__primary-balance', - ); - await driver.wait(until.elementTextMatches(balance, /0\s*BAT/u)); + await driver.waitForSelector({ + css: '.token-overview__primary-balance', + text: '0 BAT', + }); await driver.delay(regularDelayMs); }); }); @@ -1658,13 +1676,15 @@ describe('MetaMask', function () { await driver.clickElement('.btn-danger'); await driver.delay(regularDelayMs); - const confirmDeleteNetworkModal = await driver.findElement('span .modal'); + // wait for confirm delete modal to be visible. + await driver.waitForSelector('span .modal'); await driver.clickElement( '.button.btn-danger.modal-container__footer-button', ); - await driver.wait(until.stalenessOf(confirmDeleteNetworkModal)); + // wait for confirm delete modal to be removed from DOM. + await driver.waitForSelector('span .modal', { state: 'detached' }); const newNetworkListItems = await driver.findElements( '.networks-tab__networks-list-name', diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 7787468ba..9799efa24 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -1,6 +1,7 @@ const { promises: fs } = require('fs'); const { strict: assert } = require('assert'); const { until, error: webdriverError, By } = require('selenium-webdriver'); +const cssToXPath = require('css-to-xpath'); class Driver { /** @@ -28,8 +29,31 @@ class Driver { // xpath locator. return By.xpath(locator.xpath); } else if (locator.text) { - // Providing a text prop, and optionally a tag, will use xpath to look - // for an element with the tag that has matching text. + // Providing a text prop, and optionally a tag or css prop, will use + // xpath to look for an element with the tag that has matching text. + if (locator.css) { + // When providing css prop we use cssToXPath to build a xpath string + // We provide two cases to check for, first a text node of the + // element that matches the text provided OR we test the stringified + // contents of the element in the case where text is split across + // multiple children. In the later case non literal spaces are stripped + // so we do the same with the input to provide a consistent API. + const xpath = cssToXPath + .parse(locator.css) + .where( + cssToXPath.xPathBuilder + .string() + .contains(locator.text) + .or( + cssToXPath.xPathBuilder + .string() + .contains(locator.text.split(' ').join('')), + ), + ) + .toXPath(); + return By.xpath(xpath); + } + // The tag prop is optional and further refines which elements match return By.xpath( `//${locator.tag ?? '*'}[contains(text(), '${locator.text}')]`, ); @@ -47,6 +71,26 @@ class Driver { await this.driver.wait(condition, timeout); } + async waitForSelector( + rawLocator, + { timeout = this.timeout, state = 'visible' } = {}, + ) { + // Playwright has a waitForSelector method that will become a shallow + // replacement for the implementation below. It takes an option options + // bucket that can include the state attribute to wait for elements that + // match the selector to be removed from the DOM. + const selector = this.buildLocator(rawLocator); + if (state === 'visible') { + return await this.driver.wait(until.elementLocated(selector), timeout); + } else if (state === 'detached') { + return await this.driver.wait( + until.stalenessOf(await this.findElement(selector)), + timeout, + ); + } + throw new Error(`Provided state selector ${state} is not supported`); + } + async quit() { await this.driver.quit(); } diff --git a/yarn.lock b/yarn.lock index c81c268b3..48616ff2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5898,6 +5898,11 @@ bn.js@^5.1.1, bn.js@^5.1.2: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== +bo-selector@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/bo-selector/-/bo-selector-0.0.10.tgz#9816dcb00adf374ea87941a863b2acfc026afa3e" + integrity sha1-mBbcsArfN06oeUGoY7Ks/AJq+j4= + body-parser@1.19.0, body-parser@^1.16.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -7794,6 +7799,14 @@ css-select@^1.1.0, css-select@~1.2.0: domutils "1.5.1" nth-check "~1.0.1" +css-to-xpath@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/css-to-xpath/-/css-to-xpath-0.1.0.tgz#ac0d1c26cef023f7bd8cf2e1fc1f77134bc70c47" + integrity sha1-rA0cJs7wI/e9jPLh/B93E0vHDEc= + dependencies: + bo-selector "0.0.10" + xpath-builder "0.0.7" + css-vendor@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" @@ -25703,6 +25716,11 @@ xor-distance@^2.0.0: resolved "https://registry.yarnpkg.com/xor-distance/-/xor-distance-2.0.0.tgz#cad3920d3a1e3d73eeedc61a554e51972dae0798" integrity sha512-AsAqZfPAuWx7qB/0kyRDUEvoU3QKsHWzHU9smFlkaiprEpGfJ/NBbLze2Uq0rdkxCxkNM9uOLvz/KoNBCbZiLQ== +xpath-builder@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/xpath-builder/-/xpath-builder-0.0.7.tgz#67d6bbc3f6a320ec317e3e6368c5706b6111deec" + integrity sha1-Z9a7w/ajIOwxfj5jaMVwa2ER3uw= + xregexp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"