diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json --- a/cashtab/package-lock.json +++ b/cashtab/package-lock.json @@ -1,12 +1,12 @@ { "name": "cashtab", - "version": "1.4.6", + "version": "1.4.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cashtab", - "version": "1.4.6", + "version": "1.4.7", "dependencies": { "@ant-design/icons": "^5.3.0", "@bitgo/utxo-lib": "^9.33.0", diff --git a/cashtab/package.json b/cashtab/package.json --- a/cashtab/package.json +++ b/cashtab/package.json @@ -1,6 +1,6 @@ { "name": "cashtab", - "version": "1.4.6", + "version": "1.4.7", "private": true, "scripts": { "start": "node scripts/start.js", diff --git a/cashtab/src/components/Send/SendXec.js b/cashtab/src/components/Send/SendXec.js --- a/cashtab/src/components/Send/SendXec.js +++ b/cashtab/src/components/Send/SendXec.js @@ -364,43 +364,52 @@ if (validUrlParams) { // This is a tx request from the URL + + // Save this flag in state var so it can be parsed in useEffect + txInfo.parseAllAsBip21 = parseAllAsBip21; setTxInfoFromUrl(txInfo); - if (parseAllAsBip21) { - handleAddressChange({ - target: { - name: 'address', - value: txInfo.bip21, - }, - }); - } else { - // Enter address into input field and trigger handleAddressChange for validation - handleAddressChange({ + } + }, []); + + useEffect(() => { + if (txInfoFromUrl === false) { + return; + } + if (txInfoFromUrl.parseAllAsBip21) { + handleAddressChange({ + target: { + name: 'address', + value: txInfoFromUrl.bip21, + }, + }); + } else { + // Enter address into input field and trigger handleAddressChange for validation + handleAddressChange({ + target: { + name: 'address', + value: txInfoFromUrl.address, + }, + }); + if ( + 'value' in txInfoFromUrl && + !Number.isNaN(parseFloat(txInfoFromUrl.value)) + ) { + // Only update the amount field if txInfo.value is a good input + // Sometimes we want this field to be adjusted by the user, e.g. a donation amount + + // Do not populate the field if the value param is not parseable as a number + // the strings 'undefined' and 'null', which PayButton passes to signify 'no amount', fail this test + + // TODO deprecate this support once PayButton and cashtab-components do not require it + handleAmountChange({ target: { - name: 'address', - value: txInfo.address, + name: 'value', + value: txInfoFromUrl.value, }, }); - if ( - 'value' in txInfo && - !Number.isNaN(parseFloat(txInfo.value)) - ) { - // Only update the amount field if txInfo.value is a good input - // Sometimes we want this field to be adjusted by the user, e.g. a donation amount - - // Do not populate the field if the value param is not parseable as a number - // the strings 'undefined' and 'null', which PayButton passes to signify 'no amount', fail this test - - // TODO deprecate this support once PayButton and cashtab-components do not require it - handleAmountChange({ - target: { - name: 'value', - value: txInfo.value, - }, - }); - } } } - }, [cashtabCache]); + }, [txInfoFromUrl, balances.totalBalance]); function handleSendXecError(errorObj, oneToManyFlag) { // Set loading to false here as well, as balance may not change depending on where error occured in try loop diff --git a/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js b/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js --- a/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js +++ b/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js @@ -1,12 +1,20 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; -import SendXec from '../SendXec'; -import { ThemeProvider } from 'styled-components'; -import { theme } from 'assets/styles/theme'; -import { mockWalletContext } from '../fixtures/mocks'; -import { WalletContext } from 'utils/context'; -import { BrowserRouter } from 'react-router-dom'; +import { + walletWithXecAndTokens, + SEND_ADDRESS_VALIDATION_ERRORS, + SEND_AMOUNT_VALIDATION_ERRORS, +} from '../fixtures/mocks'; +import { when } from 'jest-when'; +import 'fake-indexeddb/auto'; +import localforage from 'localforage'; +import appConfig from 'config/app'; +import { + initializeCashtabStateForTests, + clearLocalForage, +} from 'components/fixtures/helpers'; +import CashtabTestWrapper from 'components/fixtures/CashtabTestWrapper'; // https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function Object.defineProperty(window, 'matchMedia', { @@ -35,20 +43,30 @@ dispatchEvent: jest.fn(), }); -const TestSendXecScreen = ( - <BrowserRouter> - <WalletContext.Provider value={mockWalletContext}> - <ThemeProvider theme={theme}> - <SendXec /> - </ThemeProvider> - </WalletContext.Provider> - </BrowserRouter> -); - -// Getting by class name is the only practical way to get some antd components -/* eslint testing-library/no-container: 0 */ describe('<SendXec /> rendered with params in URL', () => { - afterEach(() => { + beforeEach(() => { + // Mock the fetch call for Cashtab's price API + global.fetch = jest.fn(); + const fiatCode = 'usd'; // Use usd until you mock getting settings from localforage + const cryptoId = appConfig.coingeckoId; + // Keep this in the code, because different URLs will have different outputs requiring different parsing + const priceApiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${fiatCode}&include_last_updated_at=true`; + const xecPrice = 0.00003; + const priceResponse = { + ecash: { + usd: xecPrice, + last_updated_at: 1706644626, + }, + }; + when(fetch) + .calledWith(priceApiUrl) + .mockResolvedValue({ + json: () => Promise.resolve(priceResponse), + }); + }); + afterEach(async () => { + jest.clearAllMocks(); + await clearLocalForage(localforage); // Unset the window location so it does not impact other tests in this file Object.defineProperty(window, 'location', { value: { @@ -60,24 +78,27 @@ it('Legacy params. Address and value keys are set and valid.', async () => { const destinationAddress = 'ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm'; - const hash = `#/send?address=${destinationAddress}&value=500`; + const value = 500; + const hash = `#/send?address=${destinationAddress}&value=${value}`; + // ?address=ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm&value=500 Object.defineProperty(window, 'location', { value: { hash, }, writable: true, // possibility to override }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); - - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // The 'Send To' input field has this address as a value @@ -85,48 +106,65 @@ // The address input is disabled expect(addressInputEl).toHaveProperty('disabled', true); - // The amount input is empty - expect(amountInputEl).toHaveValue(500); + // The amount input is set to the expected value + expect(amountInputEl).toHaveValue(value); // The amount input is disabled expect(amountInputEl).toHaveProperty('disabled', true); - // The app-created-tx is rendered - expect(screen.getByTestId('app-created-tx')).toBeInTheDocument(); + // The "Webapp Tx Request" notice is rendered + expect(screen.getByText('Webapp Tx Request')).toBeInTheDocument(); // The Bip21Alert span is not rendered - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); - // The Send button is not disabled because we have a valid amount - expect(screen.queryByTestId('disabled-send')).not.toBeInTheDocument(); + // Wait for balance to be loaded + expect(await screen.findByText('9,513.12 XEC')).toBeInTheDocument(); - // No validation errors on load - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', - ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } + + // The Send button is not disabled because we have a valid amount + expect( + await screen.findByRole('button', { name: /Send/ }), + ).not.toHaveStyle('cursor: not-allowed'); }); - it('Legacy params. Address and value keys are set and valid. Unsupported legacy params are ignored.', async () => { + it('Legacy params. Address and value keys are set and valid. Invalid bip21 string is ignored.', async () => { const destinationAddress = 'ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm'; - const hash = `#/send?address=${destinationAddress}&value=500&bip21=isthisgoingtodosomething&someotherparam=false&anotherstill=true`; + const legacyPassedAmount = 500; + const hash = `#/send?address=${destinationAddress}&value=${legacyPassedAmount}&bip21=isthisgoingtodosomething&someotherparam=false&anotherstill=true`; + // ?address=ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm&value=500&bip21=isthisgoingtodosomething&someotherparam=false&anotherstill=true Object.defineProperty(window, 'location', { value: { hash, }, writable: true, // possibility to override }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + + // Wait for balance to be loaded + expect(await screen.findByText('9,513.12 XEC')).toBeInTheDocument(); + + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // The 'Send To' input field has this address as a value @@ -134,48 +172,56 @@ // The address input is disabled expect(addressInputEl).toHaveProperty('disabled', true); - // The amount input is empty - expect(amountInputEl).toHaveValue(500); + // The amount input is filled out per legacy passed amount + expect(amountInputEl).toHaveValue(legacyPassedAmount); // The amount input is disabled expect(amountInputEl).toHaveProperty('disabled', true); - // The app-created-tx is rendered - expect(screen.getByTestId('app-created-tx')).toBeInTheDocument(); + // The "Webapp Tx Request" notice is rendered + expect(screen.getByText('Webapp Tx Request')).toBeInTheDocument(); // The Bip21Alert span is not rendered - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); // The Send button is not disabled because we have a valid amount - expect(screen.queryByTestId('disabled-send')).not.toBeInTheDocument(); - - // No validation errors on load - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', - ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + expect( + await screen.findByRole('button', { name: /Send/ }), + ).not.toHaveStyle('cursor: not-allowed'); + + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } }); it('Legacy params. Address field is populated + disabled while value field is empty + enabled if legacy url params have address defined and value present as undefined', async () => { const destinationAddress = 'ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm'; const hash = `#/send?address=${destinationAddress}&value=undefined`; + // ?address=ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm&value=undefined Object.defineProperty(window, 'location', { value: { hash, }, writable: true, }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); - - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // The 'Send To' input field has this address as a value @@ -188,43 +234,51 @@ // The amount input is not disabled expect(amountInputEl).toHaveProperty('disabled', false); - // The app-created-tx is rendered - expect(screen.getByTestId('app-created-tx')).toBeInTheDocument(); + // The "Webapp Tx Request" notice is rendered + expect(screen.getByText('Webapp Tx Request')).toBeInTheDocument(); // The Bip21Alert span is not rendered - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); // The Send button is disabled because no amount is entered - expect(screen.getByTestId('disabled-send')).toBeInTheDocument(); - - // No validation errors on load - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', + expect(await screen.findByRole('button', { name: /Send/ })).toHaveStyle( + 'cursor: not-allowed', ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } }); it('Legacy params. Address field is populated + disabled while value field is empty + enabled if legacy url params have address defined and no value key present', async () => { const destinationAddress = 'ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm'; const hash = `#/send?address=${destinationAddress}`; + // ?address=ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm Object.defineProperty(window, 'location', { value: { hash, }, writable: true, }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); - - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // The 'Send To' input field has this address as a value @@ -237,42 +291,48 @@ // The amount input is not disabled expect(amountInputEl).toHaveProperty('disabled', false); - // The app-created-tx is rendered - expect(screen.getByTestId('app-created-tx')).toBeInTheDocument(); + // The "Webapp Tx Request" notice is rendered + expect(screen.getByText('Webapp Tx Request')).toBeInTheDocument(); // The Bip21Alert span is not rendered - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); // The Send button is disabled because no amount is entered - expect(screen.getByTestId('disabled-send')).toBeInTheDocument(); - - // No validation errors on load - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', + expect(await screen.findByRole('button', { name: /Send/ })).toHaveStyle( + 'cursor: not-allowed', ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } }); it('Legacy params. Params are ignored if only value param is present', async () => { const hash = `#/send?value=500`; + // ?value=500 Object.defineProperty(window, 'location', { value: { hash, }, writable: true, }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); - - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is rendered - expect( - screen.getByTestId('multiple-recipients-switch'), - ).toBeInTheDocument(); + expect(screen.getByText('Multiple Recipients:')).toBeInTheDocument(); // The 'Send To' input field is untouched expect(addressInputEl).toHaveValue(''); @@ -284,44 +344,50 @@ // The amount input is not disabled expect(amountInputEl).toHaveProperty('disabled', false); - // The app-created-tx is not rendered - expect(screen.queryByTestId('app-created-tx')).not.toBeInTheDocument(); + // The "Webapp Tx Request" notice is NOT rendered + expect(screen.queryByText('Webapp Tx Request')).not.toBeInTheDocument(); // The Bip21Alert span is not rendered - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); // The Send button is disabled because no amount is entered - expect(screen.getByTestId('disabled-send')).toBeInTheDocument(); - - // No validation errors on load - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', + expect(await screen.findByRole('button', { name: /Send/ })).toHaveStyle( + 'cursor: not-allowed', ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } }); it('Legacy params. Params are ignored if param is duplicated', async () => { const destinationAddress = 'ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm'; const hash = `#/send?address=${destinationAddress}&amount=500&amount=1000`; + // ?address=ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm&amount=500&amount=1000 Object.defineProperty(window, 'location', { value: { hash, }, writable: true, }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); - - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is rendered - expect( - screen.getByTestId('multiple-recipients-switch'), - ).toBeInTheDocument(); + expect(screen.getByText('Multiple Recipients:')).toBeInTheDocument(); // The 'Send To' input field is untouched expect(addressInputEl).toHaveValue(''); @@ -333,43 +399,56 @@ // The amount input is not disabled expect(amountInputEl).toHaveProperty('disabled', false); - // The app-created-tx is not rendered - expect(screen.queryByTestId('app-created-tx')).not.toBeInTheDocument(); + // The "Webapp Tx Request" notice is NOT rendered + expect(screen.queryByText('Webapp Tx Request')).not.toBeInTheDocument(); // The Bip21Alert span is not rendered - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); // The Send button is disabled because no amount is entered - expect(screen.getByTestId('disabled-send')).toBeInTheDocument(); - - // No validation errors on load - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', + expect(await screen.findByRole('button', { name: /Send/ })).toHaveStyle( + 'cursor: not-allowed', ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } }); it('Legacy params are not parsed as bip21 even if the bip21 param appears in the string', async () => { const destinationAddress = 'ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm'; - const hash = `#/send?address=${destinationAddress}&value=500&bip21=ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6?amount=17&op_return_raw=04007461622263617368746162206D6573736167652077697468206F705F72657475726E5F726177`; + const legacyPassedAmount = 500; + const hash = `#/send?address=${destinationAddress}&value=${legacyPassedAmount}&bip21=ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6?amount=17&op_return_raw=04007461622263617368746162206D6573736167652077697468206F705F72657475726E5F726177`; + // ?address=ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm&value=500&bip21=ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6?amount=17&op_return_raw=04007461622263617368746162206D6573736167652077697468206F705F72657475726E5F726177 Object.defineProperty(window, 'location', { value: { hash, }, writable: true, }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Wait for balance to be loaded + expect(await screen.findByText('9,513.12 XEC')).toBeInTheDocument(); + + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // The 'Send To' input field has this address as a value @@ -377,26 +456,32 @@ // The address input is disabled expect(addressInputEl).toHaveProperty('disabled', true); - // The amount input is empty - expect(amountInputEl).toHaveValue(500); + // The amount input has the expected value + expect(amountInputEl).toHaveValue(legacyPassedAmount); // The amount input is disabled expect(amountInputEl).toHaveProperty('disabled', true); - // The app-created-tx is rendered - expect(screen.getByTestId('app-created-tx')).toBeInTheDocument(); + // The "Webapp Tx Request" notice is rendered + expect(screen.getByText('Webapp Tx Request')).toBeInTheDocument(); // The Bip21Alert span is not rendered - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); // The Send button is not disabled because we have a valid amount - expect(screen.queryByTestId('disabled-send')).not.toBeInTheDocument(); - - // No validation errors on load - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', - ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + expect( + await screen.findByRole('button', { name: /Send/ }), + ).not.toHaveStyle('cursor: not-allowed'); + + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } }); it('bip21 param - valid bip21 param with amount and op_return_raw is parsed as expected', async () => { const destinationAddress = @@ -406,23 +491,29 @@ '04007461622263617368746162206D6573736167652077697468206F705F72657475726E5F726177'; const bip21Str = `${destinationAddress}?amount=${amount}&op_return_raw=${op_return_raw}`; const hash = `#/send?bip21=${bip21Str}`; + // ?bip21=ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm?amount=17&op_return_raw=04007461622263617368746162206D6573736167652077697468206F705F72657475726E5F726177 Object.defineProperty(window, 'location', { value: { hash, }, writable: true, }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + + // Wait for balance to be loaded + expect(await screen.findByText('9,513.12 XEC')).toBeInTheDocument(); - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // The 'Send To' input field has this address as a value @@ -435,7 +526,7 @@ // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // Amount input is the valid amount param value @@ -444,21 +535,27 @@ // The amount input is disabled because it is set by a bip21 query string expect(amountInputEl).toHaveProperty('disabled', true); - // No validation errors - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', - ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } // The Send button is enabled as we have valid address and amount params - expect(screen.queryByTestId('disabled-send')).not.toBeInTheDocument(); + expect( + await screen.findByRole('button', { name: /Send/ }), + ).not.toHaveStyle('cursor: not-allowed'); - // The app-created-tx is rendered - expect(screen.getByTestId('app-created-tx')).toBeInTheDocument(); + // The "Webapp Tx Request" notice is rendered + expect(screen.getByText('Webapp Tx Request')).toBeInTheDocument(); - // The Bip21Alert span is rendered - const bip21Alert = screen.getByTestId('bip-alert'); - expect(bip21Alert).toBeInTheDocument(); + // The Bip21Alert span is not rendered + expect( + screen.getByText('(set by BIP21 query string)'), + ).toBeInTheDocument(); // The Cashtab Message collapse is not rendered expect( @@ -483,23 +580,25 @@ // Repeat the op_return_raw param const bip21Str = `${destinationAddress}?amount=${amount}&op_return_raw=${op_return_raw}&op_return_raw=${op_return_raw}`; const hash = `#/send?bip21=${bip21Str}`; + // ?bip21=ecash:qp33mh3a7qq7p8yulhnvwty2uq5ynukqcvuxmvzfhm?amount=17&op_return_raw=04007461622263617368746162206D6573736167652077697468206F705F72657475726E5F726177&op_return_raw=04007461622263617368746162206D6573736167652077697468206F705F72657475726E5F726177 Object.defineProperty(window, 'location', { value: { hash, }, writable: true, }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); - - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // The 'Send To' input field has this address as a value @@ -512,7 +611,7 @@ // The multiple recipients switch is not rendered expect( - screen.queryByTestId('multiple-recipients-switch'), + screen.queryByText('Multiple Recipients:'), ).not.toBeInTheDocument(); // Amount input is not updated as the bip21 query is invalid @@ -521,24 +620,26 @@ // The amount input is not disabled because it is not set by the invalid bip21 query string expect(amountInputEl).toHaveProperty('disabled', false); - // Check for antd error div - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', - ); - expect(addressValidationErrorDiv).toBeInTheDocument(); - expect(addressValidationErrorDiv).toHaveTextContent( - 'bip21 parameters may not appear more than once', - ); + // We get the expected validation error + expect( + // Note that, due to antd quirks, we get 2 of these + screen.getAllByText( + 'bip21 parameters may not appear more than once', + )[0], + ).toBeInTheDocument(); // The Send button is disabled - expect(screen.getByTestId('disabled-send')).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /Send/ })).toHaveStyle( + 'cursor: not-allowed', + ); - // The app-created-tx is rendered - expect(screen.getByTestId('app-created-tx')).toBeInTheDocument(); + // The "Webapp Tx Request" notice is rendered + expect(screen.getByText('Webapp Tx Request')).toBeInTheDocument(); // The Bip21Alert span is not rendered as no info about the tx is set for invalid bip21 - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); // The Cashtab Message collapse is not rendered expect( @@ -559,18 +660,17 @@ }, writable: true, // possibility to override }); - const { container } = render(TestSendXecScreen); - const addressInputEl = screen.getByTestId('destination-address-single'); - const amountInputEl = screen.getByTestId('send-xec-input'); - - // Input fields are rendered - expect(addressInputEl).toBeInTheDocument(); - expect(amountInputEl).toBeInTheDocument(); + // Mock the app with context at the Send screen + const mockedChronik = await initializeCashtabStateForTests( + walletWithXecAndTokens, + localforage, + ); + render(<CashtabTestWrapper chronik={mockedChronik} route="/send" />); + const addressInputEl = screen.getByPlaceholderText('Address'); + const amountInputEl = screen.getByPlaceholderText('Amount'); // The multiple recipients switch is rendered - expect( - screen.getByTestId('multiple-recipients-switch'), - ).toBeInTheDocument(); + expect(screen.getByText('Multiple Recipients:')).toBeInTheDocument(); // The 'Send To' input field has this address as a value expect(addressInputEl).toHaveValue(''); @@ -582,20 +682,26 @@ // The amount input is not disabled expect(amountInputEl).toHaveProperty('disabled', false); - // The app-created-tx is not rendered - expect(screen.queryByTestId('app-created-tx')).not.toBeInTheDocument(); + // The "Webapp Tx Request" notice is NOT rendered + expect(screen.queryByText('Webapp Tx Request')).not.toBeInTheDocument(); // The Bip21Alert span is not rendered - const bip21Alert = screen.queryByTestId('bip-alert'); - expect(bip21Alert).not.toBeInTheDocument(); - - // The Send button is disable - expect(screen.getByTestId('disabled-send')).toBeInTheDocument(); + expect( + screen.queryByText('(set by BIP21 query string)'), + ).not.toBeInTheDocument(); - // No validation errors on load - const addressValidationErrorDiv = container.querySelector( - '[class="ant-form-item-explain-error"]', + // The Send button is disabled + expect(await screen.findByRole('button', { name: /Send/ })).toHaveStyle( + 'cursor: not-allowed', ); - expect(addressValidationErrorDiv).not.toBeInTheDocument(); + + // No addr validation errors on load + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); + } + // No amount validation errors on load + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); + } }); }); diff --git a/cashtab/src/components/Send/__tests__/SendToken.test.js b/cashtab/src/components/Send/__tests__/SendToken.test.js --- a/cashtab/src/components/Send/__tests__/SendToken.test.js +++ b/cashtab/src/components/Send/__tests__/SendToken.test.js @@ -54,13 +54,13 @@ // See SendToken for some modified errors (SendToken does not support bip21) // These could change, which would break tests, which is expected behavior if we haven't // updated tests properly on changing the app -const SEND_ADDRESS_VALIDATION_ERRORS = [ +const SEND_ADDRESS_VALIDATION_ERRORS_TOKEN = [ `Aliases must end with '.xec'`, 'eCash Alias does not exist or yet to receive 1 confirmation', 'Invalid address', 'eToken sends do not support bip21 query strings', ]; -const SEND_AMOUNT_VALIDATION_ERRORS = [ +const SEND_AMOUNT_VALIDATION_ERRORS_TOKEN = [ `Amount must be a number`, 'Amount must be greater than 0', `Amount cannot exceed your ${SEND_TOKEN_TICKER} balance of ${SEND_TOKEN_BALANCE}`, @@ -151,11 +151,11 @@ expect(amountInputEl).toHaveProperty('disabled', false); // No addr validation errors on load - for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS_TOKEN) { expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); } // No amount validation errors on load - for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS_TOKEN) { expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); } }); @@ -182,11 +182,11 @@ expect(addressInputEl).toHaveValue(addressInput); // No addr validation errors on load - for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS_TOKEN) { expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); } // No amount validation errors on load - for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS_TOKEN) { expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); } }); @@ -214,11 +214,11 @@ expect(addressInputEl).toHaveValue(addressInput); // No addr validation errors on load - for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS_TOKEN) { expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); } // No amount validation errors on load - for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS_TOKEN) { expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); } }); @@ -264,11 +264,11 @@ expect(addressInputEl).toHaveValue(addressInput); // No addr validation errors on load - for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS) { + for (const addrErr of SEND_ADDRESS_VALIDATION_ERRORS_TOKEN) { expect(screen.queryByText(addrErr)).not.toBeInTheDocument(); } // No amount validation errors on load - for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS) { + for (const amountErr of SEND_AMOUNT_VALIDATION_ERRORS_TOKEN) { expect(screen.queryByText(amountErr)).not.toBeInTheDocument(); } diff --git a/cashtab/src/components/Send/__tests__/SendXec.test.js b/cashtab/src/components/Send/__tests__/SendXec.test.js --- a/cashtab/src/components/Send/__tests__/SendXec.test.js +++ b/cashtab/src/components/Send/__tests__/SendXec.test.js @@ -4,7 +4,11 @@ import userEvent, { PointerEventsCheckLevel, } from '@testing-library/user-event'; -import { walletWithXecAndTokens } from '../fixtures/mocks'; +import { + walletWithXecAndTokens, + SEND_ADDRESS_VALIDATION_ERRORS, + SEND_AMOUNT_VALIDATION_ERRORS, +} from '../fixtures/mocks'; import { when } from 'jest-when'; import aliasSettings from 'config/alias'; import { explorer } from 'config/explorer'; @@ -44,18 +48,6 @@ dispatchEvent: jest.fn(), }); -// See src/validation, ref parseAddressInput -// These could change, which would break tests, which is expected behavior if we haven't -// updated tests properly on changing the app -const SEND_ADDRESS_VALIDATION_ERRORS = [ - `Aliases must end with '.xec'`, - 'eToken addresses are not supported for ${appConfig.ticker} sends', - 'Invalid address', - 'bip21 parameters may not appear more than once', - `Unsupported param`, - `Invalid op_return_raw param`, -]; -const SEND_AMOUNT_VALIDATION_ERRORS = [`Invalid XEC send amount`]; describe('<SendXec />', () => { let user; beforeEach(() => { diff --git a/cashtab/src/components/Send/fixtures/mocks.js b/cashtab/src/components/Send/fixtures/mocks.js --- a/cashtab/src/components/Send/fixtures/mocks.js +++ b/cashtab/src/components/Send/fixtures/mocks.js @@ -1,6 +1,3 @@ -import { cashtabSettings } from 'config/cashtabSettings'; -import cashtabCache from 'config/cashtabCache'; - export const walletWithXecAndTokens = { mnemonic: 'beauty shoe decline spend still weird slot snack coach flee between paper', @@ -797,8 +794,24 @@ }, }; -export const mockWalletContext = { - wallet: walletWithXecAndTokens, - cashtabState: { settings: cashtabSettings }, - cashtabCache, -}; +// See src/validation, ref parseAddressInput +// These could change, which would break tests, which is expected behavior if we haven't +// updated tests properly on changing the app +export const SEND_ADDRESS_VALIDATION_ERRORS = [ + `Aliases must end with '.xec'`, + 'eToken addresses are not supported for ${appConfig.ticker} sends', + 'Invalid address', + 'bip21 parameters may not appear more than once', + `Unsupported param`, + `Invalid op_return_raw param`, +]; + +// See function shouldRejectAmountInput in validation/index.js +export const SEND_AMOUNT_VALIDATION_ERRORS = [ + `Invalid XEC send amount`, + 'Amount must be a number', + 'Amount must be greater than 0', + `Send amount must be at least 5.50 XEC`, + `Amount cannot exceed your XEC balance`, + `XEC transactions do not support more than 2 decimal places`, +]; diff --git a/cashtab/src/validation/index.js b/cashtab/src/validation/index.js --- a/cashtab/src/validation/index.js +++ b/cashtab/src/validation/index.js @@ -124,6 +124,10 @@ fiatPrice, totalCashBalance, ) => { + console.log(`cashAmount`, cashAmount); + console.log(`selectedCurrency`, selectedCurrency); + console.log(`fiatPrice`, fiatPrice); + console.log(`totalCashBalance`, totalCashBalance); // Take cashAmount as input, a string from form input let error = false; let testedAmount = new BN(cashAmount);