Skip to content

Commit

Permalink
Improve unit tests
Browse files Browse the repository at this point in the history
The mocks used in the external provider unit tests have been rewritten
to allow sending and inspecting messages in either direction. They also
no longer rely upon mocking out internal methods.

The `getInitializedInpageProvider` helper function in the inpage
provider tests has been updated with method callbacks, to allow tests
to setup replies for specific messages. This will be used in a future
PR.

One additional test was added to ensure the `isMetaMask` property is
set on the inpage provider.
  • Loading branch information
Gudahtt committed Jul 19, 2022
1 parent e936982 commit afeb53a
Show file tree
Hide file tree
Showing 8 changed files with 353 additions and 135 deletions.
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ module.exports = {
coveragePathIgnorePatterns: ['/node_modules/', '/mocks/', '/test/'],
coverageThreshold: {
global: {
branches: 55.75,
functions: 52.81,
lines: 58.22,
statements: 58.52,
branches: 56.19,
functions: 53.93,
lines: 59.01,
statements: 59.29,
},
},
projects: [
Expand Down
21 changes: 0 additions & 21 deletions mocks/DuplexStream.ts

This file was deleted.

177 changes: 112 additions & 65 deletions src/MetaMaskInpageProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { MockDuplexStream } from '../mocks/DuplexStream';
import { JsonRpcRequest } from 'json-rpc-engine';
import { MockConnectionStream } from '../test/mocks/MockConnectionStream';
import { getDeconstructedPromise } from '../test/get-deconstructed-promise';
import {
MetaMaskInpageProviderStreamName,
MetaMaskInpageProvider,
Expand All @@ -11,45 +13,92 @@ import messages from './messages';
* {@link MetaMaskInpageProvider._initializeStateAsync}. This helper function
* returns a provider initialized with the specified values.
*
* @param options - Options bag. See {@link MetaMaskInpageProvider._initializeState}.
* @returns A tuple of the initialized provider and its stream.
* @param options - Options bag.
* @param options.initialState - The initial provider state returned on
* initialization. See {@link MetaMaskInpageProvider._initializeState}.
* @param options.onMethodCalled - A set of configuration objects for adding
* method-specific callbacks.
* @param options.onMethodCalled[].substream - The substream of the method that
* the callback is for.
* @param options.onMethodCalled[].method - The name of the method that the
* callback is for.
* @param options.onMethodCalled[].callback - The method callback.
* @returns A tuple of the initialized provider, its stream, and an "onWrite"
* stub that can be used to inspect message sent by the provider.
*/
async function getInitializedProvider({
accounts = [],
chainId = '0x0',
isUnlocked = true,
networkVersion = '0',
}: Partial<Parameters<MetaMaskInpageProvider['_initializeState']>[0]> = {}) {
// This will be called via the constructor
const requestMock = jest
.spyOn(MetaMaskInpageProvider.prototype, 'request')
.mockImplementationOnce(async () => {
return {
accounts,
chainId,
isUnlocked,
networkVersion,
};
});

const mockStream = new MockDuplexStream();
const inpageProvider = new MetaMaskInpageProvider(mockStream);
initialState: {
accounts = [],
chainId = '0x0',
isUnlocked = true,
networkVersion = '0',
} = {},
onMethodCalled = [],
}: {
initialState?: Partial<
Parameters<MetaMaskInpageProvider['_initializeState']>[0]
>;
onMethodCalled?: {
substream: string;
method: string;
callback: (data: JsonRpcRequest<unknown>) => void;
}[];
} = {}) {
let isInitialized = false;
const onWrite = jest.fn();
const connectionStream = new MockConnectionStream((name, data) => {
if (!isInitialized) {
if (
name === 'metamask-provider' &&
data.method === 'metamask_getProviderState'
) {
isInitialized = true;
// Wrap in `setImmediate` to ensure a reply is recieved by the provider
// after the provider has processed the request, to ensure that the
// provider recognizes the id.
setImmediate(() =>
connectionStream.reply('metamask-provider', {
id: onWrite.mock.calls[0][1].id,
jsonrpc: '2.0',
result: {
accounts,
chainId,
isUnlocked,
networkVersion,
},
}),
);
} else {
// Ignore other messages before initialization
return;
}
}
for (const { substream, method, callback } of onMethodCalled) {
if (name === substream && data.method === method) {
// Wrap in `setImmediate` to ensure a reply is recieved by the provider
// after the provider has processed the request, to ensure that the
// provider recognizes the id.
setImmediate(() => callback(data));
}
}
onWrite(name, data);
});

// Relinquish control of the event loop to ensure that the mocked state is
// retrieved.
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1));
const provider = new MetaMaskInpageProvider(connectionStream);
const { resolve: resolveInitialized, promise: waitForInitialization } =
getDeconstructedPromise();
provider.on('_initialized', resolveInitialized);

expect(requestMock).toHaveBeenCalledTimes(1); // Sanity check
requestMock.mockRestore(); // Get rid of the mock
await waitForInitialization;

return [inpageProvider, mockStream] as const;
return [provider, connectionStream, onWrite] as const;
}

describe('MetaMaskInpageProvider: RPC', () => {
const MOCK_ERROR_MESSAGE = 'Did you specify a mock return value?';

function initializeProvider() {
const mockStream = new MockDuplexStream();
const mockStream = new MockConnectionStream();
const provider: any | MetaMaskInpageProvider = new MetaMaskInpageProvider(
mockStream,
);
Expand Down Expand Up @@ -670,13 +719,10 @@ describe('MetaMaskInpageProvider: RPC', () => {
resolve(undefined);
});

mockStream.push({
name: MetaMaskInpageProviderStreamName,
data: {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1', networkVersion: '1' },
},
mockStream.notify(MetaMaskInpageProviderStreamName, {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1', networkVersion: '1' },
});
});
});
Expand All @@ -690,13 +736,10 @@ describe('MetaMaskInpageProvider: RPC', () => {
resolve(undefined);
});

mockStream.push({
name: MetaMaskInpageProviderStreamName,
data: {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1', networkVersion: '1' },
},
mockStream.notify(MetaMaskInpageProviderStreamName, {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1', networkVersion: '1' },
});
});
});
Expand All @@ -717,16 +760,13 @@ describe('MetaMaskInpageProvider: RPC', () => {
resolve();
});

mockStream.push({
name: MetaMaskInpageProviderStreamName,
data: {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
// A "loading" networkVersion indicates the network is changing.
// Although the chainId is different, chainChanged should not be
// emitted in this case.
params: { chainId: '0x1', networkVersion: 'loading' },
},
mockStream.notify(MetaMaskInpageProviderStreamName, {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
// A "loading" networkVersion indicates the network is changing.
// Although the chainId is different, chainChanged should not be
// emitted in this case.
params: { chainId: '0x1', networkVersion: 'loading' },
});
});

Expand All @@ -745,13 +785,10 @@ describe('MetaMaskInpageProvider: RPC', () => {
resolve();
});

mockStream.push({
name: MetaMaskInpageProviderStreamName,
data: {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1', networkVersion: '1' },
},
mockStream.notify(MetaMaskInpageProviderStreamName, {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1', networkVersion: '1' },
});
});

Expand All @@ -771,12 +808,12 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {
describe('constructor', () => {
it('succeeds if stream is provided', () => {
expect(
() => new MetaMaskInpageProvider(new MockDuplexStream()),
() => new MetaMaskInpageProvider(new MockConnectionStream()),
).not.toThrow();
});

it('succeeds if stream and valid options are provided', () => {
const stream = new MockDuplexStream();
const stream = new MockConnectionStream();

expect(
() =>
Expand Down Expand Up @@ -816,7 +853,7 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {
});

it('accepts valid custom logger', () => {
const stream = new MockDuplexStream();
const stream = new MockConnectionStream();
const customLogger = {
debug: console.debug,
error: console.error,
Expand Down Expand Up @@ -847,7 +884,7 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {
};
});

const mockStream = new MockDuplexStream();
const mockStream = new MockConnectionStream();
const inpageProvider = new MetaMaskInpageProvider(mockStream);

await new Promise<void>((resolve) => setTimeout(() => resolve(), 1));
Expand All @@ -861,7 +898,9 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {

describe('isConnected', () => {
it('returns isConnected state', () => {
const provider: any = new MetaMaskInpageProvider(new MockDuplexStream());
const provider: any = new MetaMaskInpageProvider(
new MockConnectionStream(),
);
provider.autoRefreshOnNetworkChange = false;

expect(provider.isConnected()).toBe(false);
Expand All @@ -875,4 +914,12 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {
expect(provider.isConnected()).toBe(false);
});
});

describe('isMetaMask', () => {
it('should be set to "true"', async () => {
const [provider] = await getInitializedProvider();

expect(provider.isMetaMask).toBe(true);
});
});
});
Loading

0 comments on commit afeb53a

Please sign in to comment.