1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

feat: implement swap event metric e2e test (#20129)

* implement swap event metric e2e test

* fix lint error

* clean up initial balance helpers

* fix eslint warnings

* Fix `token_to_amount` format to address firefox bug and refactor tests
This commit is contained in:
Pedro Figueiredo 2023-07-28 19:57:06 +01:00 committed by GitHub
parent 280fd5f7ef
commit 537f1c7aee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1255 additions and 23 deletions

View File

@ -246,6 +246,7 @@ module.exports = {
'shared/**/*.test.js', 'shared/**/*.test.js',
'ui/**/*.test.js', 'ui/**/*.test.js',
'ui/__mocks__/*.js', 'ui/__mocks__/*.js',
'test/e2e/helpers.test.js',
], ],
extends: ['@metamask/eslint-config-mocha'], extends: ['@metamask/eslint-config-mocha'],
rules: { rules: {
@ -271,10 +272,12 @@ module.exports = {
'app/scripts/migrations/*.test.js', 'app/scripts/migrations/*.test.js',
'app/scripts/platforms/*.test.js', 'app/scripts/platforms/*.test.js',
'development/**/*.test.js', 'development/**/*.test.js',
'development/**/*.test.ts',
'shared/**/*.test.js', 'shared/**/*.test.js',
'shared/**/*.test.ts', 'shared/**/*.test.ts',
'test/helpers/*.js', 'test/helpers/*.js',
'test/jest/*.js', 'test/jest/*.js',
'test/e2e/helpers.test.js',
'ui/**/*.test.js', 'ui/**/*.test.js',
'ui/__mocks__/*.js', 'ui/__mocks__/*.js',
'shared/lib/error-utils.test.js', 'shared/lib/error-utils.test.js',

View File

@ -9,6 +9,8 @@ module.exports = {
'./app/scripts/controllers/permissions/**/*.test.js', './app/scripts/controllers/permissions/**/*.test.js',
'./app/scripts/controllers/mmi-controller.test.js', './app/scripts/controllers/mmi-controller.test.js',
'./app/scripts/constants/error-utils.test.js', './app/scripts/constants/error-utils.test.js',
'./development/fitness-functions/**/*.test.ts',
'./test/e2e/helpers.test.js',
], ],
recursive: true, recursive: true,
require: ['test/env.js', 'test/setup.js'], require: ['test/env.js', 'test/setup.js'],

View File

@ -40,7 +40,10 @@ import {
hexWEIToDecGWEI, hexWEIToDecGWEI,
} from '../../../../shared/modules/conversion.utils'; } from '../../../../shared/modules/conversion.utils';
import { isSwapsDefaultTokenAddress } from '../../../../shared/modules/swaps.utils'; import { isSwapsDefaultTokenAddress } from '../../../../shared/modules/swaps.utils';
import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import { import {
CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP, CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP,
NETWORK_TYPES, NETWORK_TYPES,
@ -2128,7 +2131,7 @@ export default class TransactionController extends EventEmitter {
); );
this._trackMetaMetricsEvent({ this._trackMetaMetricsEvent({
event: 'Swap Completed', event: MetaMetricsEventName.SwapCompleted,
category: MetaMetricsEventCategory.Swaps, category: MetaMetricsEventCategory.Swaps,
sensitiveProperties: { sensitiveProperties: {
...txMeta.swapMetaData, ...txMeta.swapMetaData,
@ -2139,6 +2142,12 @@ export default class TransactionController extends EventEmitter {
trade_gas_cost_in_eth: transactionsCost.tradeGasCostInEth, trade_gas_cost_in_eth: transactionsCost.tradeGasCostInEth,
trade_and_approval_gas_cost_in_eth: trade_and_approval_gas_cost_in_eth:
transactionsCost.tradeAndApprovalGasCostInEth, transactionsCost.tradeAndApprovalGasCostInEth,
// Firefox and Chrome have different implementations of the APIs
// that we rely on for communication accross the app. On Chrome big
// numbers are converted into number strings, on firefox they remain
// Big Number objects. As such, we convert them here for both
// browsers.
token_to_amount: txMeta.swapMetaData.token_to_amount.toString(10),
}, },
}); });
} }

View File

@ -14,6 +14,7 @@ module.exports = {
'<rootDir>/shared/**/*.(js|ts|tsx)', '<rootDir>/shared/**/*.(js|ts|tsx)',
'<rootDir>/ui/**/*.(js|ts|tsx)', '<rootDir>/ui/**/*.(js|ts|tsx)',
'<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)', '<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)',
'<rootDir>/test/e2e/helpers.test.js',
], ],
coverageDirectory: './coverage', coverageDirectory: './coverage',
coveragePathIgnorePatterns: ['.stories.*', '.snap'], coveragePathIgnorePatterns: ['.stories.*', '.snap'],
@ -50,6 +51,7 @@ module.exports = {
'<rootDir>/shared/**/*.test.(js|ts)', '<rootDir>/shared/**/*.test.(js|ts)',
'<rootDir>/ui/**/*.test.(js|ts|tsx)', '<rootDir>/ui/**/*.test.(js|ts|tsx)',
'<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)', '<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)',
'<rootDir>/test/e2e/helpers.test.js',
], ],
testTimeout: 5500, testTimeout: 5500,
// We have to specify the environment we are running in, which is jsdom. The // We have to specify the environment we are running in, which is jsdom. The

View File

@ -612,6 +612,18 @@ export enum MetaMetricsEventName {
ActivityScreenOpened = 'Activity Screen Opened', ActivityScreenOpened = 'Activity Screen Opened',
WhatsNewViewed = `What's New Viewed`, WhatsNewViewed = `What's New Viewed`,
WhatsNewClicked = `What's New Link Clicked`, WhatsNewClicked = `What's New Link Clicked`,
PrepareSwapPageLoaded = 'Prepare Swap Page Loaded',
QuotesRequested = 'Quotes Requested',
QuotesReceived = 'Quotes Received',
BestQuoteReviewed = 'Best Quote Reviewed',
AllAvailableQuotesOpened = 'All Available Quotes Opened',
SwapStarted = 'Swap Started',
TransactionAdded = 'Transaction Added',
TransactionSubmitted = 'Transaction Submitted',
TransactionApproved = 'Transaction Approved',
SwapCompleted = 'Swap Completed',
TransactionFinalized = 'Transaction Finalized',
ExitedSwaps = 'Exited Swaps',
} }
export enum MetaMetricsEventAccountType { export enum MetaMetricsEventAccountType {

View File

@ -589,9 +589,10 @@ function mockPhishingDetection(mockServer) {
const PRIVATE_KEY = const PRIVATE_KEY =
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC'; '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC';
const generateETHBalance = (eth) => convertToHexValue(eth * 10 ** 18); const convertETHToHexGwei = (eth) => convertToHexValue(eth * 10 ** 18);
const defaultGanacheOptions = { const defaultGanacheOptions = {
accounts: [{ secretKey: PRIVATE_KEY, balance: generateETHBalance(25) }], accounts: [{ secretKey: PRIVATE_KEY, balance: convertETHToHexGwei(25) }],
}; };
const SERVICE_WORKER_URL = 'chrome://inspect/#service-workers'; const SERVICE_WORKER_URL = 'chrome://inspect/#service-workers';
@ -654,7 +655,7 @@ const DEFAULT_GANACHE_OPTIONS = {
accounts: [ accounts: [
{ {
secretKey: DEFAULT_PRIVATE_KEY, secretKey: DEFAULT_PRIVATE_KEY,
balance: generateETHBalance(25), balance: convertETHToHexGwei(25),
}, },
], ],
}; };
@ -687,6 +688,10 @@ const logInWithBalanceValidation = async (driver, ganacheServer) => {
await assertAccountBalanceForDOM(driver, ganacheServer); await assertAccountBalanceForDOM(driver, ganacheServer);
}; };
async function sleepSeconds(sec) {
return new Promise((resolve) => setTimeout(resolve, sec * 1000));
}
function roundToXDecimalPlaces(number, decimalPlaces) { function roundToXDecimalPlaces(number, decimalPlaces) {
return Math.round(number * 10 ** decimalPlaces) / 10 ** decimalPlaces; return Math.round(number * 10 ** decimalPlaces) / 10 ** decimalPlaces;
} }
@ -699,8 +704,15 @@ function generateRandNumBetween(x, y) {
return randomNumber; return randomNumber;
} }
async function sleepSeconds(sec) { function genRandInitBal(minETHBal = 10, maxETHBal = 100, decimalPlaces = 4) {
return new Promise((resolve) => setTimeout(resolve, sec * 1000)); const initialBalance = roundToXDecimalPlaces(
generateRandNumBetween(minETHBal, maxETHBal),
decimalPlaces,
);
const initialBalanceInHex = convertETHToHexGwei(initialBalance);
return { initialBalance, initialBalanceInHex };
} }
async function terminateServiceWorker(driver) { async function terminateServiceWorker(driver) {
@ -762,12 +774,44 @@ async function getEventPayloads(driver, mockedEndpoints, hasRequest = true) {
} }
return isPending === !hasRequest; return isPending === !hasRequest;
}, 10000); }, driver.timeout);
const mockedRequests = []; const mockedRequests = [];
for (const mockedEndpoint of mockedEndpoints) { for (const mockedEndpoint of mockedEndpoints) {
mockedRequests.push(...(await mockedEndpoint.getSeenRequests())); mockedRequests.push(...(await mockedEndpoint.getSeenRequests()));
} }
return mockedRequests.map((req) => req.body.json.batch).flat();
return mockedRequests.map((req) => req.body.json?.batch).flat();
}
// Asserts that each request passes all assertions in one group of assertions, and the order does not matter.
function assertInAnyOrder(requests, assertions) {
// Clone the array to avoid mutating the original
const assertionsClone = [...assertions];
return (
requests.every((request) => {
for (let a = 0; a < assertionsClone.length; a++) {
const assertionArray = assertionsClone[a];
const passed = assertionArray.reduce(
(acc, currAssertionFn) => currAssertionFn(request) && acc,
true,
);
if (passed) {
// Remove the used assertion array
assertionsClone.splice(a, 1);
// Exit the loop early since we found a matching assertion
return true;
}
}
// No matching assertion found for this request
return false;
}) &&
// Ensure all assertions were used
assertionsClone.length === 0
);
} }
module.exports = { module.exports = {
@ -810,7 +854,7 @@ module.exports = {
WALLET_PASSWORD, WALLET_PASSWORD,
WINDOW_TITLES, WINDOW_TITLES,
DEFAULT_GANACHE_OPTIONS, DEFAULT_GANACHE_OPTIONS,
generateETHBalance, convertETHToHexGwei,
roundToXDecimalPlaces, roundToXDecimalPlaces,
generateRandNumBetween, generateRandNumBetween,
sleepSeconds, sleepSeconds,
@ -823,4 +867,6 @@ module.exports = {
onboardingRevealAndConfirmSRP, onboardingRevealAndConfirmSRP,
onboardingCompleteWalletCreation, onboardingCompleteWalletCreation,
onboardingPinExtension, onboardingPinExtension,
assertInAnyOrder,
genRandInitBal,
}; };

53
test/e2e/helpers.test.js Normal file
View File

@ -0,0 +1,53 @@
import { assertInAnyOrder } from './helpers';
describe('assertInAnyOrder()', () => {
it('returns true when all requests pass unique assertions', () => {
const requests = ['req1', 'req2', 'req3'];
const assertions = [
[(req) => req === 'req1'],
[(req) => req === 'req2'],
[(req) => req === 'req3'],
];
expect(assertInAnyOrder(requests, assertions)).toBe(true);
});
it('returns true when all requests pass unique assertions independently of the order', () => {
const requests = ['req1', 'req2', 'req3'];
const assertions = [
[(req) => req === 'req3'],
[(req) => req === 'req2'],
[(req) => req === 'req1'],
];
expect(assertInAnyOrder(requests, assertions)).toBe(true);
});
it('returns false when a request cannot pass any assertions', () => {
const requests = ['req1', 'req2', 'unknown'];
const assertions = [
[(req) => req === 'req1'],
[(req) => req === 'req2'],
[(req) => req === 'req3'],
];
expect(assertInAnyOrder(requests, assertions)).toBe(false);
});
it('returns false when there are unused assertions', () => {
const requests = ['req1', 'req2'];
const assertions = [
[(req) => req === 'req1'],
[(req) => req === 'req2'],
[(req) => req === 'req3'],
];
expect(assertInAnyOrder(requests, assertions)).toBe(false);
});
it('returns false when there are unused requests', () => {
const requests = ['req1', 'req2', 'req3', 'req4'];
const assertions = [
[(req) => req === 'req1'],
[(req) => req === 'req2'],
[(req) => req === 'req3'],
];
expect(assertInAnyOrder(requests, assertions)).toBe(false);
});
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,572 @@
const { strict: assert } = require('assert');
const { toHex } = require('@metamask/controller-utils');
const FixtureBuilder = require('../fixture-builder');
const {
withFixtures,
generateGanacheOptions,
DEFAULT_GANACHE_OPTIONS,
unlockWallet,
getEventPayloads,
assertInAnyOrder,
genRandInitBal,
} = require('../helpers');
const {
buildQuote,
reviewQuote,
waitForTransactionToComplete,
checkActivityTransaction,
changeExchangeRate,
} = require('../swaps/shared');
const {
MetaMetricsEventCategory,
MetaMetricsEventName,
} = require('../../../shared/constants/metametrics');
const {
TOKENS_API_MOCK_RESULT,
TOP_ASSETS_API_MOCK_RESULT,
AGGREGATOR_METADATA_API_MOCK_RESULT,
GAS_PRICE_API_MOCK_RESULT,
FEATURE_FLAGS_API_MOCK_RESULT,
NETWORKS_API_MOCK_RESULT,
TRADES_API_MOCK_RESULT,
NETWORKS_2_API_MOCK_RESULT,
} = require('./mock-data');
const numberOfSegmentRequests = 19;
async function mockSegmentAndMetaswapRequests(mockServer) {
return [
await mockServer
.forPost('https://api.segment.io/v1/batch')
.withJsonBodyIncluding({
batch: [{ properties: { category: MetaMetricsEventCategory.Swaps } }],
})
.times()
.thenCallback(() => ({ statusCode: 200 })),
await mockServer
.forGet('https://swap.metaswap.codefi.network/networks/1/tokens')
.thenCallback(() => ({ statusCode: 200, json: TOKENS_API_MOCK_RESULT })),
await mockServer
.forGet('https://swap.metaswap.codefi.network/networks/1/topAssets')
.thenCallback(() => ({
statusCode: 200,
json: TOP_ASSETS_API_MOCK_RESULT,
})),
await mockServer
.forGet(
'https://swap.metaswap.codefi.network/networks/1/aggregatorMetadata',
)
.thenCallback(() => ({
statusCode: 200,
json: AGGREGATOR_METADATA_API_MOCK_RESULT,
})),
await mockServer
.forGet('https://gas-api.metaswap.codefi.network/networks/1/gasPrices')
.thenCallback(() => ({
statusCode: 200,
json: GAS_PRICE_API_MOCK_RESULT,
})),
await mockServer
.forGet('https://swap.metaswap.codefi.network/featureFlags')
.thenCallback(() => ({
statusCode: 200,
json: FEATURE_FLAGS_API_MOCK_RESULT,
})),
await mockServer
.forGet('https://tx-insights.metaswap.codefi.network/networks')
.thenCallback(() => ({
statusCode: 200,
json: NETWORKS_API_MOCK_RESULT,
})),
await mockServer
.forGet('https://swap.metaswap.codefi.network/networks/1/trades')
.thenCallback(() => ({
statusCode: 200,
json: TRADES_API_MOCK_RESULT,
})),
await mockServer
.forGet('https://swap.metaswap.codefi.network/networks/1')
.thenCallback(() => ({
statusCode: 200,
json: NETWORKS_2_API_MOCK_RESULT,
})),
await mockServer
.forGet('https://token-api.metaswap.codefi.network/token/1337')
.thenCallback(() => ({
statusCode: 200,
json: {},
})),
];
}
describe('Swap Eth for another Token', function () {
it('Completes a Swap between ETH and DAI after changing initial rate', async function () {
const { initialBalanceInHex } = genRandInitBal();
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
ganacheOptions: generateGanacheOptions({
accounts: [
{
secretKey: DEFAULT_GANACHE_OPTIONS.accounts[0].secretKey,
balance: initialBalanceInHex,
},
],
}),
title: this.test.title,
testSpecificMock: mockSegmentAndMetaswapRequests,
},
async ({ driver, mockedEndpoint: mockedEndpoints }) => {
await driver.navigate();
await unlockWallet(driver);
await getQuoteAndSwapTokens(driver);
const metricsReqs = await assertReqsNumAndFilterMetrics(
driver,
mockedEndpoints,
);
await assertNavSwapButtonClickedEvent(metricsReqs);
await assertPrepareSwapPageLoadedEvents(metricsReqs);
await assertQuotesRequestedEvents(metricsReqs);
await assertQuotesReceivedAndBestQuoteReviewedEvents(metricsReqs);
await assertAllAvailableQuotesOpenedEvents(metricsReqs);
await assertSwapStartedEvents(metricsReqs);
await assertSwapCompletedEvents(metricsReqs);
await assertExitedSwapsEvents(metricsReqs);
},
);
});
});
async function getQuoteAndSwapTokens(driver) {
await buildQuote(driver, {
amount: 2,
swapTo: 'DAI',
});
await reviewQuote(driver, {
amount: 2,
swapFrom: 'TESTETH',
swapTo: 'DAI',
});
await changeExchangeRate(driver);
await reviewQuote(driver, {
amount: 2,
swapFrom: 'TESTETH',
swapTo: 'DAI',
skipCounter: true,
});
await driver.clickElement({ text: 'Swap', tag: 'button' });
await waitForTransactionToComplete(driver, { tokenName: 'DAI' });
await checkActivityTransaction(driver, {
index: 0,
amount: '2',
swapFrom: 'TESTETH',
swapTo: 'DAI',
});
}
async function assertReqsNumAndFilterMetrics(driver, mockedEndpoints) {
const events = await getEventPayloads(driver, mockedEndpoints);
const numberOfMetaswapRequests = 9;
assert.equal(
events.length,
numberOfSegmentRequests + numberOfMetaswapRequests,
);
const reqs = events.slice(0, numberOfSegmentRequests);
return reqs;
}
async function assertNavSwapButtonClickedEvent(reqs) {
assert.equal(reqs[0].event, MetaMetricsEventName.NavSwapButtonClicked);
assert.deepStrictEqual(reqs[0].properties, {
category: MetaMetricsEventCategory.Swaps,
chain_id: toHex(1337),
environment_type: 'fullscreen',
locale: 'en',
location: 'Main View',
text: 'Swap',
token_symbol: 'ETH',
});
}
async function assertPrepareSwapPageLoadedEvents(reqs) {
const assertionsReq1 = [
(req) => req.event === MetaMetricsEventName.PrepareSwapPageLoaded,
(req) => Object.keys(req.properties).length === 7,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
(req) => req.properties?.current_stx_enabled === false,
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.stx_enabled === false,
];
const assertionsReq2 = [
(req) => req.event === MetaMetricsEventName.PrepareSwapPageLoaded,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
];
assert.ok(
assertInAnyOrder([reqs[1], reqs[2]], [assertionsReq1, assertionsReq2]),
'assertPrepareSwapPageLoadedEvents(): reqs[1] and reqs[2] did not match what was expected',
);
}
async function assertQuotesRequestedEvents(reqs) {
const assertionsReq3 = [
(req) => req.event === MetaMetricsEventName.QuotesRequested,
(req) => Object.keys(req.properties).length === 14,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
(req) => req.properties?.anonymizedData === true,
(req) => req.properties?.current_stx_enabled === false,
(req) => req.properties?.custom_slippage === false,
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.request_type === 'Order',
(req) => req.properties?.slippage === 2,
(req) => req.properties?.stx_enabled === false,
(req) => req.properties?.token_from === 'TESTETH',
(req) => req.properties?.token_from_amount === '2',
(req) => req.properties?.token_to === 'DAI',
];
const assertionsReq4 = [
(req) => req.event === MetaMetricsEventName.QuotesRequested,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
];
assert.ok(
assertInAnyOrder([reqs[3], reqs[4]], [assertionsReq3, assertionsReq4]),
'assertQuotesRequestedEvents(): reqs[3] and reqs[4] did not match what was expected',
);
}
async function assertQuotesReceivedAndBestQuoteReviewedEvents(reqs) {
const assertionsReq5 = [
(req) => req.event === MetaMetricsEventName.QuotesReceived,
(req) => Object.keys(req.properties).length === 18,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
(req) => req.properties?.anonymizedData === true,
(req) => typeof req.properties?.available_quotes === 'number',
(req) => typeof req.properties?.best_quote_source === 'string',
(req) => req.properties?.current_stx_enabled === false,
(req) => req.properties?.custom_slippage === false,
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.request_type === 'Order',
(req) => typeof req.properties?.response_time === 'number',
(req) => req.properties?.slippage === 2,
(req) => req.properties?.stx_enabled === false,
(req) => req.properties?.token_from === 'TESTETH',
(req) => req.properties?.token_from_amount === '2',
(req) => req.properties?.token_to === 'DAI',
(req) => typeof req.properties?.token_to_amount === 'string',
];
const assertionsReq6 = [
(req) => req.event === MetaMetricsEventName.QuotesReceived,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
];
const assertionsReq7 = [
(req) => req.event === MetaMetricsEventName.BestQuoteReviewed,
(req) => Object.keys(req.properties).length === 17,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
(req) => typeof req.properties?.available_quotes === 'number',
(req) => typeof req.properties?.best_quote_source === 'string',
(req) => req.properties?.current_stx_enabled === false,
(req) => req.properties?.custom_slippage === false,
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.request_type === false,
(req) => req.properties?.slippage === 2,
(req) => req.properties?.stx_enabled === false,
(req) => req.properties?.token_from === 'TESTETH',
(req) => req.properties?.token_from_amount === '2',
(req) => req.properties?.token_to === 'DAI',
(req) => typeof req.properties?.token_to_amount === 'string',
];
const assertionsReq8 = [
(req) => req.event === MetaMetricsEventName.BestQuoteReviewed,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
];
// When running this test on Chrome in particular, reqs[5], reqs[6], reqs[7]
// and reqs[8] sometimes switch order so we bundled them together for the
// assertion
assert.ok(
assertInAnyOrder(
[reqs[5], reqs[6], reqs[7], reqs[8]],
[assertionsReq5, assertionsReq6, assertionsReq7, assertionsReq8],
),
'assertQuotesReceivedAndBestQuoteReviewedEvents(): reqs[5], reqs[6], reqs[7] and reqs[8] did not match what was expected',
);
}
async function assertAllAvailableQuotesOpenedEvents(reqs) {
const assertionsReq9 = [
(req) => req.event === MetaMetricsEventName.AllAvailableQuotesOpened,
(req) => Object.keys(req.properties).length === 18,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
(req) => typeof req.properties?.available_quotes === 'number',
(req) => typeof req.properties?.best_quote_source === 'string',
(req) => req.properties?.current_stx_enabled === false,
(req) => req.properties?.custom_slippage === false,
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.request_type === false,
(req) => req.properties?.slippage === 2,
(req) => req.properties?.stx_enabled === false,
(req) => req.properties?.token_from === 'TESTETH',
(req) => req.properties?.token_from_amount === '2',
(req) => req.properties?.token_to === 'DAI',
(req) => req.properties?.token_to === 'DAI',
(req) => req.properties?.other_quote_selected === false,
(req) => req.properties?.other_quote_selected_source === null,
(req) => typeof req.properties?.token_to_amount === 'string',
];
const assertionsReq10 = [
(req) => req.event === MetaMetricsEventName.AllAvailableQuotesOpened,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
];
assert.ok(
assertInAnyOrder([reqs[9], reqs[10]], [assertionsReq9, assertionsReq10]),
'assertAllAvailableQuotesOpenedEvents(): reqs[9] and reqs[10] did not match what was expected',
);
}
async function assertSwapStartedEvents(reqs) {
const assertionsReq11 = [
(req) => req.event === MetaMetricsEventName.SwapStarted,
(req) => Object.keys(req.properties).length === 24,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
(req) => req.properties?.token_from === 'TESTETH',
(req) => req.properties?.token_from_amount === '2',
(req) => req.properties?.token_to === 'DAI',
(req) => req.properties?.slippage === 2,
(req) => req.properties?.custom_slippage === false,
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.stx_enabled === false,
(req) => req.properties?.current_stx_enabled === false,
(req) => typeof req.properties?.token_to_amount === 'string',
(req) => typeof req.properties?.best_quote_source === 'string',
(req) => typeof req.properties?.other_quote_selected === 'boolean',
(req) => typeof req.properties?.gas_fees === 'string',
(req) => typeof req.properties?.estimated_gas === 'string',
(req) => typeof req.properties?.suggested_gas_price === 'string',
(req) => typeof req.properties?.reg_tx_fee_in_usd === 'number',
(req) => typeof req.properties?.reg_tx_fee_in_eth === 'number',
(req) => typeof req.properties?.reg_tx_max_fee_in_usd === 'number',
(req) => typeof req.properties?.reg_tx_max_fee_in_eth === 'number',
(req) => typeof req.properties?.other_quote_selected_source === 'string',
];
const assertionsReq12 = [
(req) => req.event === MetaMetricsEventName.SwapStarted,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
];
assert.ok(
assertInAnyOrder([reqs[11], reqs[12]], [assertionsReq11, assertionsReq12]),
'assertSwapStartedEvents(): reqs[11] and reqs[12] did not match what was expected',
);
}
async function assertSwapCompletedEvents(reqs) {
const assertionsReq13 = [
(req) => req.event === MetaMetricsEventName.SwapCompleted,
(req) => Object.keys(req.properties).length === 30,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'background',
(req) => req.properties?.locale === 'en',
(req) => req.properties?.token_from === 'TESTETH',
(req) => req.properties?.token_from_amount === '2',
(req) => req.properties?.token_to === 'DAI',
(req) => typeof req.properties?.token_to_amount === 'string',
(req) => req.properties?.slippage === 2,
(req) => req.properties?.custom_slippage === false,
(req) => req.properties?.best_quote_source === 'airswapV4',
(req) => typeof req.properties?.other_quote_selected === 'boolean',
(req) => typeof req.properties?.other_quote_selected_source === 'string',
(req) => typeof req.properties?.gas_fees === 'string',
(req) => typeof req.properties?.estimated_gas === 'string',
(req) => req.properties?.suggested_gas_price === '30',
(req) => req.properties?.used_gas_price === '30',
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.stx_enabled === false,
(req) => req.properties?.current_stx_enabled === false,
(req) => typeof req.properties?.reg_tx_fee_in_usd === 'number',
(req) => typeof req.properties?.reg_tx_fee_in_eth === 'number',
(req) => typeof req.properties?.reg_tx_max_fee_in_usd === 'number',
(req) => typeof req.properties?.reg_tx_max_fee_in_eth === 'number',
(req) => req.properties?.token_to_amount_received === '',
(req) => req.properties?.quote_vs_executionRatio === null,
(req) => req.properties?.estimated_vs_used_gasRatio === '100%',
(req) => req.properties?.approval_gas_cost_in_eth === 0,
(req) => typeof req.properties?.trade_gas_cost_in_eth === 'number',
(req) =>
typeof req.properties?.trade_and_approval_gas_cost_in_eth === 'number',
];
const assertionsReq14 = [
(req) => req.event === MetaMetricsEventName.SwapCompleted,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'background',
(req) => req.properties?.locale === 'en',
];
assert.ok(
assertInAnyOrder([reqs[13], reqs[14]], [assertionsReq13, assertionsReq14]),
'assertSwapCompletedEvents(): reqs[13] and reqs[14] did not match what was expected',
);
}
async function assertExitedSwapsEvents(reqs) {
const assertionsReq15 = [
(req) => req.event === MetaMetricsEventName.ExitedSwaps,
(req) => Object.keys(req.properties).length === 12,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
(req) => req.properties?.token_from_amount === '2',
(req) => req.properties?.request_type === false,
(req) => req.properties?.slippage === 2,
(req) => req.properties?.custom_slippage === false,
(req) => req.properties?.current_screen === 'awaiting-swap',
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.stx_enabled === false,
(req) => req.properties?.current_stx_enabled === false,
];
const assertionsReq16 = [
(req) => req.event === MetaMetricsEventName.ExitedSwaps,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
];
assert.ok(
assertInAnyOrder([reqs[15], reqs[16]], [assertionsReq15, assertionsReq16]),
'assertExitedSwapsEvents(): reqs[15] and reqs[16] did not match what was expected',
);
const assertionsReq17 = [
(req) => req.event === MetaMetricsEventName.ExitedSwaps,
(req) => Object.keys(req.properties).length === 9,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
(req) => req.properties?.custom_slippage === true,
(req) => req.properties?.current_screen === 'awaiting-swap',
(req) => req.properties?.is_hardware_wallet === false,
(req) => req.properties?.stx_enabled === false,
(req) => req.properties?.current_stx_enabled === false,
];
const assertionsReq18 = [
(req) => req.event === MetaMetricsEventName.ExitedSwaps,
(req) => Object.keys(req.properties).length === 4,
(req) => req.properties?.category === MetaMetricsEventCategory.Swaps,
(req) => req.properties?.chain_id === toHex(1337),
(req) => req.properties?.environment_type === 'fullscreen',
(req) => req.properties?.locale === 'en',
];
assert.ok(
assertInAnyOrder([reqs[17], reqs[18]], [assertionsReq17, assertionsReq18]),
'assertExitedSwapsEvents(): reqs[17] and reqs[18] did not match what was expected',
);
}

View File

@ -6,22 +6,19 @@ const {
WALLET_PASSWORD, WALLET_PASSWORD,
WINDOW_TITLES, WINDOW_TITLES,
DEFAULT_GANACHE_OPTIONS, DEFAULT_GANACHE_OPTIONS,
generateETHBalance,
roundToXDecimalPlaces,
generateRandNumBetween, generateRandNumBetween,
sleepSeconds, sleepSeconds,
terminateServiceWorker, terminateServiceWorker,
unlockWallet, unlockWallet,
largeDelayMs, largeDelayMs,
genRandInitBal,
roundToXDecimalPlaces,
} = require('../helpers'); } = require('../helpers');
const FixtureBuilder = require('../fixture-builder'); const FixtureBuilder = require('../fixture-builder');
describe('MV3 - Restart service worker multiple times', function () { describe('MV3 - Restart service worker multiple times', function () {
it('Simple simple send flow within full screen view should still be usable', async function () { it('Simple simple send flow within full screen view should still be usable', async function () {
const initialBalance = roundToXDecimalPlaces( const { initialBalance, initialBalanceInHex } = genRandInitBal();
generateRandNumBetween(10, 100),
4,
);
await withFixtures( await withFixtures(
{ {
@ -30,7 +27,7 @@ describe('MV3 - Restart service worker multiple times', function () {
accounts: [ accounts: [
{ {
secretKey: DEFAULT_GANACHE_OPTIONS.accounts[0].secretKey, secretKey: DEFAULT_GANACHE_OPTIONS.accounts[0].secretKey,
balance: generateETHBalance(initialBalance), balance: initialBalanceInHex,
}, },
], ],
}), }),

View File

@ -63,7 +63,10 @@ import {
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
} from '../../selectors'; } from '../../selectors';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../shared/constants/metametrics';
import { import {
ERROR_FETCHING_QUOTES, ERROR_FETCHING_QUOTES,
QUOTES_NOT_AVAILABLE_ERROR, QUOTES_NOT_AVAILABLE_ERROR,
@ -787,6 +790,18 @@ export const fetchQuotesAndSetQuoteState = (
} else { } else {
const newSelectedQuote = fetchedQuotes[selectedAggId]; const newSelectedQuote = fetchedQuotes[selectedAggId];
const tokenToAmountBN = calcTokenAmount(
newSelectedQuote.destinationAmount,
newSelectedQuote.decimals || 18,
);
// Firefox and Chrome have different implementations of the APIs
// that we rely on for communication accross the app. On Chrome big
// numbers are converted into number strings, on firefox they remain
// Big Number objects. As such, we convert them here for both
// browsers.
const tokenToAmountToString = tokenToAmountBN.toString(10);
trackEvent({ trackEvent({
event: 'Quotes Received', event: 'Quotes Received',
category: MetaMetricsEventCategory.Swaps, category: MetaMetricsEventCategory.Swaps,
@ -794,10 +809,7 @@ export const fetchQuotesAndSetQuoteState = (
token_from: fromTokenSymbol, token_from: fromTokenSymbol,
token_from_amount: String(inputValue), token_from_amount: String(inputValue),
token_to: toTokenSymbol, token_to: toTokenSymbol,
token_to_amount: calcTokenAmount( token_to_amount: tokenToAmountToString,
newSelectedQuote.destinationAmount,
newSelectedQuote.decimals || 18,
),
request_type: balanceError ? 'Quote' : 'Order', request_type: balanceError ? 'Quote' : 'Order',
slippage: maxSlippage, slippage: maxSlippage,
custom_slippage: maxSlippage !== Slippage.default, custom_slippage: maxSlippage !== Slippage.default,
@ -1177,7 +1189,7 @@ export const signAndSendTransactions = (
} }
trackEvent({ trackEvent({
event: 'Swap Started', event: MetaMetricsEventName.SwapStarted,
category: MetaMetricsEventCategory.Swaps, category: MetaMetricsEventCategory.Swaps,
sensitiveProperties: swapMetaData, sensitiveProperties: swapMetaData,
}); });