mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-10-22 11:22:43 +02:00
Reduce calls of the /featureFlag
API (#10859)
* Remove periodic calls to the /featureFlag API * Always show the Swap button on the main page * Check if the Swaps feature is enabled, show loading animation while waiting * Reuse an existing useEffect call * Use ‘isFeatureFlagLoaded’ in React’s state, resolve lint issues * Add a watch mode for Jest testing * Add unit tests for Swaps: fetchSwapsLiveness, add /ducks/swaps into Jest testing * Remove Swaps Jest tests from Mocha’s ESLint rules * Ignore Swaps Jest tests while running Mocha, update paths * Increase test coverage to the current max * Fix ESLint issues for Swaps * Enable the Swaps feature by default and after state reset, remove loading screen before seeing Swaps * Update Jest config, fix tests * Update Jest coverage threshold to the current maximum * Update ESLint rule in jest.config.js * Disable the “Review Swap” button if the feature flag hasn’t loaded yet * Update jest threshold
This commit is contained in:
parent
9e918b6026
commit
e7d7d24d83
@ -108,14 +108,14 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.test.js'],
|
files: ['**/*.test.js'],
|
||||||
excludedFiles: ['ui/app/pages/swaps/**/*.test.js'],
|
excludedFiles: ['ui/app/**/swaps/**/*.test.js'],
|
||||||
extends: ['@metamask/eslint-config-mocha'],
|
extends: ['@metamask/eslint-config-mocha'],
|
||||||
rules: {
|
rules: {
|
||||||
'mocha/no-setup-in-describe': 'off',
|
'mocha/no-setup-in-describe': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['ui/app/pages/swaps/**/*.test.js'],
|
files: ['ui/app/**/swaps/**/*.test.js'],
|
||||||
extends: ['@metamask/eslint-config-jest'],
|
extends: ['@metamask/eslint-config-jest'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -70,7 +70,7 @@ const initialState = {
|
|||||||
errorKey: '',
|
errorKey: '',
|
||||||
topAggId: null,
|
topAggId: null,
|
||||||
routeState: '',
|
routeState: '',
|
||||||
swapsFeatureIsLive: false,
|
swapsFeatureIsLive: true,
|
||||||
swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME,
|
swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -112,8 +112,6 @@ export default class SwapsController {
|
|||||||
this.ethersProvider = new ethers.providers.Web3Provider(provider);
|
this.ethersProvider = new ethers.providers.Web3Provider(provider);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._setupSwapsLivenessFetching();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the refresh rate for quote updates from the MetaSwap API
|
// Sets the refresh rate for quote updates from the MetaSwap API
|
||||||
@ -478,7 +476,6 @@ export default class SwapsController {
|
|||||||
swapsState: {
|
swapsState: {
|
||||||
...initialState.swapsState,
|
...initialState.swapsState,
|
||||||
tokens: swapsState.tokens,
|
tokens: swapsState.tokens,
|
||||||
swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
|
|
||||||
swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime,
|
swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -686,99 +683,6 @@ export default class SwapsController {
|
|||||||
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId],
|
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the fetching of the swaps feature liveness flag from our API.
|
|
||||||
* Performs an initial fetch when called, then fetches on a 10-minute
|
|
||||||
* interval.
|
|
||||||
*
|
|
||||||
* If the browser goes offline, the interval is cleared and swaps are disabled
|
|
||||||
* until the value can be fetched again.
|
|
||||||
*/
|
|
||||||
_setupSwapsLivenessFetching() {
|
|
||||||
const TEN_MINUTES_MS = 10 * 60 * 1000;
|
|
||||||
let intervalId = null;
|
|
||||||
|
|
||||||
const fetchAndSetupInterval = () => {
|
|
||||||
if (window.navigator.onLine && intervalId === null) {
|
|
||||||
// Set the interval first to prevent race condition between listener and
|
|
||||||
// initial call to this function.
|
|
||||||
intervalId = setInterval(
|
|
||||||
this._fetchAndSetSwapsLiveness.bind(this),
|
|
||||||
TEN_MINUTES_MS,
|
|
||||||
);
|
|
||||||
this._fetchAndSetSwapsLiveness();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('online', fetchAndSetupInterval);
|
|
||||||
window.addEventListener('offline', () => {
|
|
||||||
if (intervalId !== null) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
intervalId = null;
|
|
||||||
|
|
||||||
const { swapsState } = this.store.getState();
|
|
||||||
if (swapsState.swapsFeatureIsLive) {
|
|
||||||
this.setSwapsLiveness(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchAndSetupInterval();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function should only be called via _setupSwapsLivenessFetching.
|
|
||||||
*
|
|
||||||
* Attempts to fetch the swaps feature liveness flag from our API. Tries
|
|
||||||
* to fetch three times at 5-second intervals before giving up, in which
|
|
||||||
* case the value defaults to 'false'.
|
|
||||||
*
|
|
||||||
* Only updates state if the fetched/computed flag value differs from current
|
|
||||||
* state.
|
|
||||||
*/
|
|
||||||
async _fetchAndSetSwapsLiveness() {
|
|
||||||
const { swapsState } = this.store.getState();
|
|
||||||
const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState;
|
|
||||||
const chainId = this._getCurrentChainId();
|
|
||||||
|
|
||||||
let swapsFeatureIsLive = false;
|
|
||||||
let successfullyFetched = false;
|
|
||||||
let numAttempts = 0;
|
|
||||||
|
|
||||||
const fetchAndIncrementNumAttempts = async () => {
|
|
||||||
try {
|
|
||||||
swapsFeatureIsLive = Boolean(
|
|
||||||
await this._fetchSwapsFeatureLiveness(chainId),
|
|
||||||
);
|
|
||||||
successfullyFetched = true;
|
|
||||||
} catch (err) {
|
|
||||||
log.error(err);
|
|
||||||
numAttempts += 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await fetchAndIncrementNumAttempts();
|
|
||||||
|
|
||||||
// The loop conditions are modified by fetchAndIncrementNumAttempts.
|
|
||||||
// eslint-disable-next-line no-unmodified-loop-condition
|
|
||||||
while (!successfullyFetched && numAttempts < 3) {
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, 5000); // 5 seconds
|
|
||||||
});
|
|
||||||
await fetchAndIncrementNumAttempts();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!successfullyFetched) {
|
|
||||||
log.error(
|
|
||||||
'Failed to fetch swaps feature flag 3 times. Setting to false and trying again next interval.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (swapsFeatureIsLive !== oldSwapsFeatureIsLive) {
|
|
||||||
this.setSwapsLiveness(swapsFeatureIsLive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -124,7 +124,7 @@ const EMPTY_INIT_STATE = {
|
|||||||
errorKey: '',
|
errorKey: '',
|
||||||
topAggId: null,
|
topAggId: null,
|
||||||
routeState: '',
|
routeState: '',
|
||||||
swapsFeatureIsLive: false,
|
swapsFeatureIsLive: true,
|
||||||
swapsQuoteRefreshTime: 60000,
|
swapsQuoteRefreshTime: 60000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -897,281 +897,6 @@ describe('SwapsController', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('_setupSwapsLivenessFetching ', function () {
|
|
||||||
let clock;
|
|
||||||
const EXPECTED_TIME = 600000;
|
|
||||||
|
|
||||||
const getLivenessState = () => {
|
|
||||||
return swapsController.store.getState().swapsState.swapsFeatureIsLive;
|
|
||||||
};
|
|
||||||
|
|
||||||
// We have to do this to overwrite window.navigator.onLine
|
|
||||||
const stubWindow = () => {
|
|
||||||
sandbox.replace(global, 'window', {
|
|
||||||
addEventListener: window.addEventListener,
|
|
||||||
navigator: { onLine: true },
|
|
||||||
dispatchEvent: window.dispatchEvent,
|
|
||||||
Event: window.Event,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
stubWindow();
|
|
||||||
clock = sandbox.useFakeTimers();
|
|
||||||
sandbox.spy(clock, 'setInterval');
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(SwapsController.prototype, '_fetchAndSetSwapsLiveness')
|
|
||||||
.resolves(undefined);
|
|
||||||
|
|
||||||
sandbox.spy(SwapsController.prototype, '_setupSwapsLivenessFetching');
|
|
||||||
|
|
||||||
sandbox.spy(window, 'addEventListener');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls _setupSwapsLivenessFetching in constructor', function () {
|
|
||||||
swapsController = getSwapsController();
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
swapsController._setupSwapsLivenessFetching.calledOnce,
|
|
||||||
'should have called _setupSwapsLivenessFetching once',
|
|
||||||
);
|
|
||||||
assert.ok(window.addEventListener.calledWith('online'));
|
|
||||||
assert.ok(window.addEventListener.calledWith('offline'));
|
|
||||||
assert.ok(
|
|
||||||
clock.setInterval.calledOnceWithExactly(
|
|
||||||
sinon.match.func,
|
|
||||||
EXPECTED_TIME,
|
|
||||||
),
|
|
||||||
'should have set an interval',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles browser being offline on boot, then coming online', async function () {
|
|
||||||
window.navigator.onLine = false;
|
|
||||||
|
|
||||||
swapsController = getSwapsController();
|
|
||||||
assert.ok(
|
|
||||||
swapsController._setupSwapsLivenessFetching.calledOnce,
|
|
||||||
'should have called _setupSwapsLivenessFetching once',
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
swapsController._fetchAndSetSwapsLiveness.notCalled,
|
|
||||||
'should not have called _fetchAndSetSwapsLiveness',
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
clock.setInterval.notCalled,
|
|
||||||
'should not have set an interval',
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
false,
|
|
||||||
'swaps feature should be disabled',
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchPromise = new Promise((resolve) => {
|
|
||||||
const originalFunction = swapsController._fetchAndSetSwapsLiveness;
|
|
||||||
swapsController._fetchAndSetSwapsLiveness = () => {
|
|
||||||
originalFunction();
|
|
||||||
resolve();
|
|
||||||
swapsController._fetchAndSetSwapsLiveness = originalFunction;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// browser comes online
|
|
||||||
window.navigator.onLine = true;
|
|
||||||
window.dispatchEvent(new window.Event('online'));
|
|
||||||
await fetchPromise;
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
swapsController._fetchAndSetSwapsLiveness.calledOnce,
|
|
||||||
'should have called _fetchAndSetSwapsLiveness once',
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
clock.setInterval.calledOnceWithExactly(
|
|
||||||
sinon.match.func,
|
|
||||||
EXPECTED_TIME,
|
|
||||||
),
|
|
||||||
'should have set an interval',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clears interval if browser goes offline', async function () {
|
|
||||||
swapsController = getSwapsController();
|
|
||||||
|
|
||||||
// set feature to live
|
|
||||||
const { swapsState } = swapsController.store.getState();
|
|
||||||
swapsController.store.updateState({
|
|
||||||
swapsState: { ...swapsState, swapsFeatureIsLive: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
sandbox.spy(swapsController.store, 'updateState');
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
clock.setInterval.calledOnceWithExactly(
|
|
||||||
sinon.match.func,
|
|
||||||
EXPECTED_TIME,
|
|
||||||
),
|
|
||||||
'should have set an interval',
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearIntervalPromise = new Promise((resolve) => {
|
|
||||||
const originalFunction = clock.clearInterval;
|
|
||||||
clock.clearInterval = (intervalId) => {
|
|
||||||
originalFunction(intervalId);
|
|
||||||
clock.clearInterval = originalFunction;
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// browser goes offline
|
|
||||||
window.navigator.onLine = false;
|
|
||||||
window.dispatchEvent(new window.Event('offline'));
|
|
||||||
|
|
||||||
// if this resolves, clearInterval was called
|
|
||||||
await clearIntervalPromise;
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
swapsController._fetchAndSetSwapsLiveness.calledOnce,
|
|
||||||
'should have called _fetchAndSetSwapsLiveness once',
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
swapsController.store.updateState.calledOnce,
|
|
||||||
'should have called updateState once',
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
false,
|
|
||||||
'swaps feature should be disabled',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('_fetchAndSetSwapsLiveness', function () {
|
|
||||||
const getLivenessState = () => {
|
|
||||||
return swapsController.store.getState().swapsState.swapsFeatureIsLive;
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
fetchSwapsFeatureLivenessStub.reset();
|
|
||||||
sandbox.stub(SwapsController.prototype, '_setupSwapsLivenessFetching');
|
|
||||||
swapsController = getSwapsController();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fetches feature liveness as expected when API is live', async function () {
|
|
||||||
fetchSwapsFeatureLivenessStub.resolves(true);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
false,
|
|
||||||
'liveness should be false on boot',
|
|
||||||
);
|
|
||||||
|
|
||||||
await swapsController._fetchAndSetSwapsLiveness();
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
fetchSwapsFeatureLivenessStub.calledOnce,
|
|
||||||
'should have called fetch function once',
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
true,
|
|
||||||
'liveness should be true after call',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not update state if fetched value is same as state value', async function () {
|
|
||||||
fetchSwapsFeatureLivenessStub.resolves(false);
|
|
||||||
sandbox.spy(swapsController.store, 'updateState');
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
false,
|
|
||||||
'liveness should be false on boot',
|
|
||||||
);
|
|
||||||
|
|
||||||
await swapsController._fetchAndSetSwapsLiveness();
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
fetchSwapsFeatureLivenessStub.calledOnce,
|
|
||||||
'should have called fetch function once',
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
swapsController.store.updateState.notCalled,
|
|
||||||
'should not have called store.updateState',
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
false,
|
|
||||||
'liveness should remain false after call',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tries three times before giving up if fetching fails', async function () {
|
|
||||||
const clock = sandbox.useFakeTimers();
|
|
||||||
fetchSwapsFeatureLivenessStub.rejects(new Error('foo'));
|
|
||||||
sandbox.spy(swapsController.store, 'updateState');
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
false,
|
|
||||||
'liveness should be false on boot',
|
|
||||||
);
|
|
||||||
|
|
||||||
swapsController._fetchAndSetSwapsLiveness();
|
|
||||||
await clock.runAllAsync();
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
fetchSwapsFeatureLivenessStub.calledThrice,
|
|
||||||
'should have called fetch function three times',
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
swapsController.store.updateState.notCalled,
|
|
||||||
'should not have called store.updateState',
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
false,
|
|
||||||
'liveness should remain false after call',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets state after fetching on successful retry', async function () {
|
|
||||||
const clock = sandbox.useFakeTimers();
|
|
||||||
fetchSwapsFeatureLivenessStub.onCall(0).rejects(new Error('foo'));
|
|
||||||
fetchSwapsFeatureLivenessStub.onCall(1).rejects(new Error('foo'));
|
|
||||||
fetchSwapsFeatureLivenessStub.onCall(2).resolves(true);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
false,
|
|
||||||
'liveness should be false on boot',
|
|
||||||
);
|
|
||||||
|
|
||||||
swapsController._fetchAndSetSwapsLiveness();
|
|
||||||
await clock.runAllAsync();
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
fetchSwapsFeatureLivenessStub.callCount,
|
|
||||||
3,
|
|
||||||
'should have called fetch function three times',
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getLivenessState(),
|
|
||||||
true,
|
|
||||||
'liveness should be true after call',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('utils', function () {
|
describe('utils', function () {
|
||||||
|
@ -3,12 +3,12 @@ module.exports = {
|
|||||||
coverageDirectory: 'jest-coverage/',
|
coverageDirectory: 'jest-coverage/',
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
branches: 6.3,
|
branches: 5.83,
|
||||||
functions: 9.43,
|
functions: 8.28,
|
||||||
lines: 8.66,
|
lines: 11.18,
|
||||||
statements: 8.88,
|
statements: 11.21,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setupFiles: ['./test/setup.js', './test/env.js'],
|
setupFiles: ['./test/setup.js', './test/env.js'],
|
||||||
testMatch: ['**/ui/app/pages/swaps/**/?(*.)+(test).js'],
|
testMatch: ['<rootDir>/ui/app/**/swaps/**/*.test.js'],
|
||||||
};
|
};
|
||||||
|
@ -20,11 +20,12 @@
|
|||||||
"forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010",
|
"forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010",
|
||||||
"dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'",
|
"dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'",
|
||||||
"sendwithprivatedapp": "node development/static-server.js test/e2e/send-eth-with-private-key-test --port 8080",
|
"sendwithprivatedapp": "node development/static-server.js test/e2e/send-eth-with-private-key-test --port 8080",
|
||||||
"test:unit": "mocha --exit --require test/env.js --require test/setup.js --ignore './ui/app/pages/swaps/**/*.test.js' --recursive './{ui,app,shared}/**/*.test.js'",
|
"test:unit": "mocha --exit --require test/env.js --require test/setup.js --ignore './ui/app/**/swaps/**/*.test.js' --recursive './{ui,app,shared}/**/*.test.js'",
|
||||||
"test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive test/unit-global/*.test.js",
|
"test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive test/unit-global/*.test.js",
|
||||||
"test:unit:jest": "jest",
|
"test:unit:jest": "jest",
|
||||||
|
"test:unit:jest:watch": "jest --watch",
|
||||||
"test:unit:jest:ci": "jest --maxWorkers=2",
|
"test:unit:jest:ci": "jest --maxWorkers=2",
|
||||||
"test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --ignore './app/scripts/controllers/permissions/*.test.js' --ignore './ui/app/pages/swaps/**/*.test.js' --recursive './{ui,app,shared}/**/*.test.js'",
|
"test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --ignore './app/scripts/controllers/permissions/*.test.js' --ignore './ui/app/**/swaps/**/*.test.js' --recursive './{ui,app,shared}/**/*.test.js'",
|
||||||
"test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive './app/scripts/controllers/permissions/*.test.js'",
|
"test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive './app/scripts/controllers/permissions/*.test.js'",
|
||||||
"test:unit:path": "mocha --exit --require test/env.js --require test/setup.js --recursive",
|
"test:unit:path": "mocha --exit --require test/env.js --require test/setup.js --recursive",
|
||||||
"test:e2e:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-all.sh",
|
"test:e2e:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-all.sh",
|
||||||
@ -32,7 +33,7 @@
|
|||||||
"test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh",
|
"test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh",
|
||||||
"test:e2e:firefox:metrics": "SELENIUM_BROWSER=firefox mocha test/e2e/metrics.spec.js",
|
"test:e2e:firefox:metrics": "SELENIUM_BROWSER=firefox mocha test/e2e/metrics.spec.js",
|
||||||
"test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html",
|
"test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html",
|
||||||
"test:coverage:jest": "jest --coverage --maxWorkers=2 --collectCoverageFrom=**/ui/app/pages/swaps/**",
|
"test:coverage:jest": "jest --coverage --maxWorkers=2 --collectCoverageFrom=**/ui/app/**/swaps/**",
|
||||||
"test:coverage:strict": "nyc --check-coverage yarn test:unit:strict",
|
"test:coverage:strict": "nyc --check-coverage yarn test:unit:strict",
|
||||||
"test:coverage:path": "nyc --check-coverage yarn test:unit:path",
|
"test:coverage:path": "nyc --check-coverage yarn test:unit:path",
|
||||||
"ganache:start": "./development/run-ganache.sh",
|
"ganache:start": "./development/run-ganache.sh",
|
||||||
|
@ -32,10 +32,7 @@ import {
|
|||||||
import SwapIcon from '../../ui/icon/swap-icon.component';
|
import SwapIcon from '../../ui/icon/swap-icon.component';
|
||||||
import BuyIcon from '../../ui/icon/overview-buy-icon.component';
|
import BuyIcon from '../../ui/icon/overview-buy-icon.component';
|
||||||
import SendIcon from '../../ui/icon/overview-send-icon.component';
|
import SendIcon from '../../ui/icon/overview-send-icon.component';
|
||||||
import {
|
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
|
||||||
getSwapsFeatureLiveness,
|
|
||||||
setSwapsFromToken,
|
|
||||||
} from '../../../ducks/swaps/swaps';
|
|
||||||
import IconButton from '../../ui/icon-button';
|
import IconButton from '../../ui/icon-button';
|
||||||
import WalletOverview from './wallet-overview';
|
import WalletOverview from './wallet-overview';
|
||||||
|
|
||||||
@ -73,7 +70,6 @@ const EthOverview = ({ className }) => {
|
|||||||
properties: { source: 'Main View', active_currency: 'ETH' },
|
properties: { source: 'Main View', active_currency: 'ETH' },
|
||||||
category: 'swaps',
|
category: 'swaps',
|
||||||
});
|
});
|
||||||
const swapsEnabled = useSelector(getSwapsFeatureLiveness);
|
|
||||||
const defaultSwapsToken = useSelector(getSwapsDefaultToken);
|
const defaultSwapsToken = useSelector(getSwapsDefaultToken);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -138,34 +134,32 @@ const EthOverview = ({ className }) => {
|
|||||||
history.push(SEND_ROUTE);
|
history.push(SEND_ROUTE);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{swapsEnabled ? (
|
<IconButton
|
||||||
<IconButton
|
className="eth-overview__button"
|
||||||
className="eth-overview__button"
|
disabled={!isSwapsChain}
|
||||||
disabled={!isSwapsChain}
|
Icon={SwapIcon}
|
||||||
Icon={SwapIcon}
|
onClick={() => {
|
||||||
onClick={() => {
|
if (isSwapsChain) {
|
||||||
if (isSwapsChain) {
|
enteredSwapsEvent();
|
||||||
enteredSwapsEvent();
|
dispatch(setSwapsFromToken(defaultSwapsToken));
|
||||||
dispatch(setSwapsFromToken(defaultSwapsToken));
|
if (usingHardwareWallet) {
|
||||||
if (usingHardwareWallet) {
|
global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE);
|
||||||
global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE);
|
} else {
|
||||||
} else {
|
history.push(BUILD_QUOTE_ROUTE);
|
||||||
history.push(BUILD_QUOTE_ROUTE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
label={t('swap')}
|
}}
|
||||||
tooltipRender={(contents) => (
|
label={t('swap')}
|
||||||
<Tooltip
|
tooltipRender={(contents) => (
|
||||||
title={t('onlyAvailableOnMainnet')}
|
<Tooltip
|
||||||
position="bottom"
|
title={t('onlyAvailableOnMainnet')}
|
||||||
disabled={isSwapsChain}
|
position="bottom"
|
||||||
>
|
disabled={isSwapsChain}
|
||||||
{contents}
|
>
|
||||||
</Tooltip>
|
{contents}
|
||||||
)}
|
</Tooltip>
|
||||||
/>
|
)}
|
||||||
) : null}
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
className={className}
|
className={className}
|
||||||
|
@ -18,10 +18,7 @@ import {
|
|||||||
import { useTokenTracker } from '../../../hooks/useTokenTracker';
|
import { useTokenTracker } from '../../../hooks/useTokenTracker';
|
||||||
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
|
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
|
||||||
import { updateSendToken } from '../../../store/actions';
|
import { updateSendToken } from '../../../store/actions';
|
||||||
import {
|
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
|
||||||
getSwapsFeatureLiveness,
|
|
||||||
setSwapsFromToken,
|
|
||||||
} from '../../../ducks/swaps/swaps';
|
|
||||||
import {
|
import {
|
||||||
getAssetImages,
|
getAssetImages,
|
||||||
getCurrentKeyring,
|
getCurrentKeyring,
|
||||||
@ -63,7 +60,6 @@ const TokenOverview = ({ className, token }) => {
|
|||||||
properties: { source: 'Token View', active_currency: token.symbol },
|
properties: { source: 'Token View', active_currency: token.symbol },
|
||||||
category: 'swaps',
|
category: 'swaps',
|
||||||
});
|
});
|
||||||
const swapsEnabled = useSelector(getSwapsFeatureLiveness);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WalletOverview
|
<WalletOverview
|
||||||
@ -96,41 +92,39 @@ const TokenOverview = ({ className, token }) => {
|
|||||||
label={t('send')}
|
label={t('send')}
|
||||||
data-testid="eth-overview-send"
|
data-testid="eth-overview-send"
|
||||||
/>
|
/>
|
||||||
{swapsEnabled ? (
|
<IconButton
|
||||||
<IconButton
|
className="token-overview__button"
|
||||||
className="token-overview__button"
|
disabled={!isSwapsChain}
|
||||||
disabled={!isSwapsChain}
|
Icon={SwapIcon}
|
||||||
Icon={SwapIcon}
|
onClick={() => {
|
||||||
onClick={() => {
|
if (isSwapsChain) {
|
||||||
if (isSwapsChain) {
|
enteredSwapsEvent();
|
||||||
enteredSwapsEvent();
|
dispatch(
|
||||||
dispatch(
|
setSwapsFromToken({
|
||||||
setSwapsFromToken({
|
...token,
|
||||||
...token,
|
iconUrl: assetImages[token.address],
|
||||||
iconUrl: assetImages[token.address],
|
balance,
|
||||||
balance,
|
string: balanceToRender,
|
||||||
string: balanceToRender,
|
}),
|
||||||
}),
|
);
|
||||||
);
|
if (usingHardwareWallet) {
|
||||||
if (usingHardwareWallet) {
|
global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE);
|
||||||
global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE);
|
} else {
|
||||||
} else {
|
history.push(BUILD_QUOTE_ROUTE);
|
||||||
history.push(BUILD_QUOTE_ROUTE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
label={t('swap')}
|
}}
|
||||||
tooltipRender={(contents) => (
|
label={t('swap')}
|
||||||
<Tooltip
|
tooltipRender={(contents) => (
|
||||||
title={t('onlyAvailableOnMainnet')}
|
<Tooltip
|
||||||
position="bottom"
|
title={t('onlyAvailableOnMainnet')}
|
||||||
disabled={isSwapsChain}
|
position="bottom"
|
||||||
>
|
disabled={isSwapsChain}
|
||||||
{contents}
|
>
|
||||||
</Tooltip>
|
{contents}
|
||||||
)}
|
</Tooltip>
|
||||||
/>
|
)}
|
||||||
) : null}
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
className={className}
|
className={className}
|
||||||
|
@ -362,6 +362,21 @@ export const fetchAndSetSwapsGasPriceInfo = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchSwapsLiveness = () => {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
let swapsFeatureIsLive = false;
|
||||||
|
try {
|
||||||
|
swapsFeatureIsLive = await fetchSwapsFeatureLiveness(
|
||||||
|
getCurrentChainId(getState()),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
|
||||||
|
}
|
||||||
|
await dispatch(setSwapsLiveness(swapsFeatureIsLive));
|
||||||
|
return swapsFeatureIsLive;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchQuotesAndSetQuoteState = (
|
export const fetchQuotesAndSetQuoteState = (
|
||||||
history,
|
history,
|
||||||
inputValue,
|
inputValue,
|
||||||
|
118
ui/app/ducks/swaps/swaps.test.js
Normal file
118
ui/app/ducks/swaps/swaps.test.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import nock from 'nock';
|
||||||
|
|
||||||
|
import { setSwapsLiveness } from '../../store/actions';
|
||||||
|
import { setStorageItem } from '../../../lib/storage-helpers';
|
||||||
|
import * as swaps from './swaps';
|
||||||
|
|
||||||
|
jest.mock('../../store/actions.js', () => ({
|
||||||
|
setSwapsLiveness: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const providerState = {
|
||||||
|
chainId: '0x1',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: '',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'mainnet',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Ducks - Swaps', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
nock.cleanAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchSwapsLiveness', () => {
|
||||||
|
const cleanFeatureFlagApiCache = () => {
|
||||||
|
setStorageItem(
|
||||||
|
'cachedFetch:https://api.metaswap.codefi.network/featureFlag',
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanFeatureFlagApiCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockFeatureFlagApiResponse = ({
|
||||||
|
active = false,
|
||||||
|
replyWithError = false,
|
||||||
|
} = {}) => {
|
||||||
|
const apiNock = nock('https://api.metaswap.codefi.network').get(
|
||||||
|
'/featureFlag',
|
||||||
|
);
|
||||||
|
if (replyWithError) {
|
||||||
|
return apiNock.replyWithError({
|
||||||
|
message: 'Server error. Try again later',
|
||||||
|
code: 'serverSideError',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return apiNock.reply(200, {
|
||||||
|
active,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createGetState = () => {
|
||||||
|
return () => ({
|
||||||
|
metamask: { provider: { ...providerState } },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('returns true if the Swaps feature is enabled', async () => {
|
||||||
|
const mockDispatch = jest.fn();
|
||||||
|
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: true });
|
||||||
|
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()(
|
||||||
|
mockDispatch,
|
||||||
|
createGetState(),
|
||||||
|
);
|
||||||
|
expect(featureFlagApiNock.isDone()).toBe(true);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setSwapsLiveness).toHaveBeenCalledWith(true);
|
||||||
|
expect(isSwapsFeatureEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the Swaps feature is disabled', async () => {
|
||||||
|
const mockDispatch = jest.fn();
|
||||||
|
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: false });
|
||||||
|
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()(
|
||||||
|
mockDispatch,
|
||||||
|
createGetState(),
|
||||||
|
);
|
||||||
|
expect(featureFlagApiNock.isDone()).toBe(true);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setSwapsLiveness).toHaveBeenCalledWith(false);
|
||||||
|
expect(isSwapsFeatureEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the /featureFlag API call throws an error', async () => {
|
||||||
|
const mockDispatch = jest.fn();
|
||||||
|
const featureFlagApiNock = mockFeatureFlagApiResponse({
|
||||||
|
replyWithError: true,
|
||||||
|
});
|
||||||
|
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()(
|
||||||
|
mockDispatch,
|
||||||
|
createGetState(),
|
||||||
|
);
|
||||||
|
expect(featureFlagApiNock.isDone()).toBe(true);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setSwapsLiveness).toHaveBeenCalledWith(false);
|
||||||
|
expect(isSwapsFeatureEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only calls the API once and returns true from cache for the second call', async () => {
|
||||||
|
const mockDispatch = jest.fn();
|
||||||
|
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: true });
|
||||||
|
await swaps.fetchSwapsLiveness()(mockDispatch, createGetState());
|
||||||
|
expect(featureFlagApiNock.isDone()).toBe(true);
|
||||||
|
const featureFlagApiNock2 = mockFeatureFlagApiResponse({ active: true });
|
||||||
|
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()(
|
||||||
|
mockDispatch,
|
||||||
|
createGetState(),
|
||||||
|
);
|
||||||
|
expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead.
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||||
|
expect(setSwapsLiveness).toHaveBeenCalledWith(true);
|
||||||
|
expect(isSwapsFeatureEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -75,6 +75,7 @@ export default function BuildQuote({
|
|||||||
setMaxSlippage,
|
setMaxSlippage,
|
||||||
maxSlippage,
|
maxSlippage,
|
||||||
selectedAccountAddress,
|
selectedAccountAddress,
|
||||||
|
isFeatureFlagLoaded,
|
||||||
}) {
|
}) {
|
||||||
const t = useContext(I18nContext);
|
const t = useContext(I18nContext);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -529,6 +530,7 @@ export default function BuildQuote({
|
|||||||
}}
|
}}
|
||||||
submitText={t('swapReviewSwap')}
|
submitText={t('swapReviewSwap')}
|
||||||
disabled={
|
disabled={
|
||||||
|
!isFeatureFlagLoaded ||
|
||||||
!Number(inputValue) ||
|
!Number(inputValue) ||
|
||||||
!selectedToToken?.address ||
|
!selectedToToken?.address ||
|
||||||
Number(maxSlippage) === 0 ||
|
Number(maxSlippage) === 0 ||
|
||||||
@ -549,4 +551,5 @@ BuildQuote.propTypes = {
|
|||||||
ethBalance: PropTypes.string,
|
ethBalance: PropTypes.string,
|
||||||
setMaxSlippage: PropTypes.func,
|
setMaxSlippage: PropTypes.func,
|
||||||
selectedAccountAddress: PropTypes.string,
|
selectedAccountAddress: PropTypes.string,
|
||||||
|
isFeatureFlagLoaded: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
getSwapsFeatureLiveness,
|
getSwapsFeatureLiveness,
|
||||||
prepareToLeaveSwaps,
|
prepareToLeaveSwaps,
|
||||||
fetchAndSetSwapsGasPriceInfo,
|
fetchAndSetSwapsGasPriceInfo,
|
||||||
|
fetchSwapsLiveness,
|
||||||
} from '../../ducks/swaps/swaps';
|
} from '../../ducks/swaps/swaps';
|
||||||
import {
|
import {
|
||||||
AWAITING_SWAP_ROUTE,
|
AWAITING_SWAP_ROUTE,
|
||||||
@ -85,6 +86,7 @@ export default function Swap() {
|
|||||||
|
|
||||||
const [inputValue, setInputValue] = useState(fetchParams?.value || '');
|
const [inputValue, setInputValue] = useState(fetchParams?.value || '');
|
||||||
const [maxSlippage, setMaxSlippage] = useState(fetchParams?.slippage || 3);
|
const [maxSlippage, setMaxSlippage] = useState(fetchParams?.slippage || 3);
|
||||||
|
const [isFeatureFlagLoaded, setIsFeatureFlagLoaded] = useState(false);
|
||||||
|
|
||||||
const routeState = useSelector(getBackgroundSwapRouteState);
|
const routeState = useSelector(getBackgroundSwapRouteState);
|
||||||
const selectedAccount = useSelector(getSelectedAccount);
|
const selectedAccount = useSelector(getSelectedAccount);
|
||||||
@ -200,10 +202,15 @@ export default function Swap() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const fetchSwapsLivenessWrapper = async () => {
|
||||||
|
await dispatch(fetchSwapsLiveness());
|
||||||
|
setIsFeatureFlagLoaded(true);
|
||||||
|
};
|
||||||
|
fetchSwapsLivenessWrapper();
|
||||||
return () => {
|
return () => {
|
||||||
exitEventRef.current();
|
exitEventRef.current();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (swapsErrorKey && !isSwapsErrorRoute) {
|
if (swapsErrorKey && !isSwapsErrorRoute) {
|
||||||
@ -283,6 +290,7 @@ export default function Swap() {
|
|||||||
setMaxSlippage={setMaxSlippage}
|
setMaxSlippage={setMaxSlippage}
|
||||||
selectedAccountAddress={selectedAccountAddress}
|
selectedAccountAddress={selectedAccountAddress}
|
||||||
maxSlippage={maxSlippage}
|
maxSlippage={maxSlippage}
|
||||||
|
isFeatureFlagLoaded={isFeatureFlagLoaded}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
Loading…
Reference in New Issue
Block a user