Skip to content

Commit

Permalink
feat: Accept function param when mocking requests to enable dynamic m…
Browse files Browse the repository at this point in the history
…ocks (#44)

* fix: Update formatting in connect-wallet.test.tsx

The prettierrc file has "tabWidth": 2, but the tabs in this file were 4.

* feat: Accept function param when mocking requests to enable dynamic mocks

Add the ability to mock with a callback instead of static data, so that we can do useful things like mock signing a payload
  • Loading branch information
texuf authored Jul 7, 2022
1 parent 3773041 commit 613ac0e
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {
act,
render,
screen,
waitFor,
waitForElementToBeRemoved,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { generateTestingUtils } from "eth-testing";
import { ethers } from "ethers";
import WalletConnection from "..";

describe("Connect wallet", () => {
describe("Connect wallet [Metamask]", () => {
let originalEthereum: any;
const testingUtils = generateTestingUtils({
providerType: "MetaMask",
Expand Down Expand Up @@ -113,4 +115,35 @@ describe("Connect wallet", () => {
await screen.findByText(/chain id: 0x3/i);
expect(screen.getByText(/balance: 5.00/i)).toBeInTheDocument();
});
test("User should be able to sign a transaction", async () => {
const bobsWallet = ethers.Wallet.createRandom();
// Start with a connected bobsWallet
testingUtils.mockConnectedWallet([bobsWallet.address]);
// mock out a function to call when we request personal sign
let signedMessages: string[] = [];
testingUtils.lowLevel.mockRequest("personal_sign", async (params: any) => {
let statement = (params as string[])[0];
const signedMessage = await bobsWallet.signMessage(statement);
signedMessages.push(signedMessage);
return signedMessage;
});
// render the wallet
render(<WalletConnection />);
// connect the wallet
const connectButton = screen.getByRole("button", { name: /connect/i });
userEvent.click(connectButton);
await screen.findByText(`account: ${bobsWallet.address}`, { exact: false });
// get the div for the signature result
const signatureResult = screen.getByTestId("signatureResult");
expect(signatureResult).toHaveTextContent("");
// sign the transaction
const signButton = screen.getByRole("button", { name: /sign/i });
userEvent.click(signButton);
// wait for the result to appear
await waitFor(() => {
expect(signatureResult).not.toHaveTextContent("");
});
// expect it to be the same as the signed message
expect(signatureResult).toHaveTextContent(signedMessages[0]);
});
});
12 changes: 12 additions & 0 deletions examples/react-apps/src/pages/metamask-connection/wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,24 @@ function useWallet() {

function Wallet() {
const { account, chainId, balance } = useWallet();
const [signatureResult, setSignatureResult] = React.useState("");
const personalSign = React.useCallback(async () => {
const ethereum = window.ethereum;
const message = "Hello, world!";
const result = await ethereum.request({
method: "personal_sign",
params: [message, account, ""],
});
setSignatureResult(result);
}, [account]);

return (
<div>
<div>Account: {account}</div>
<div>Chain ID: {chainId}</div>
<div>Balance: {(Number(balance) / 10 ** 18).toFixed(2)}</div>
<div data-testid="signatureResult">{signatureResult}</div>
<button onClick={personalSign}>Sign</button>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,108 +1,144 @@
import {
act,
render,
screen,
waitForElementToBeRemoved,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { generateTestingUtils } from "eth-testing";
import WalletConnection from "..";
import * as walletConnectProvider from '../provider';

describe("Connect wallet", () => {
const testingUtils = generateTestingUtils({
providerType: "WalletConnect",
act,
render,
screen,
waitFor,
waitForElementToBeRemoved,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { generateTestingUtils } from "eth-testing";
import { ethers } from "ethers";
import WalletConnection from "..";
import * as walletConnectProvider from "../provider";

describe("Connect wallet [Wallet Connect]", () => {
const testingUtils = generateTestingUtils({
providerType: "WalletConnect",
});

beforeEach(() => {
jest
.spyOn(walletConnectProvider, "getWalletConnectProvider")
.mockReturnValue(testingUtils.getProvider() as unknown as any);
});

afterEach(() => {
testingUtils.clearAllMocks();
});

test("User should be able to connect using Wallet Connect", async () => {
// Start with not connected wallet
testingUtils.mockNotConnectedWallet();

testingUtils.mockAccounts(["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"]);
testingUtils.mockChainId(1);
testingUtils.mockBlockNumber(1);
testingUtils.mockBalance(
"0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf",
"0x1bc16d674ec80000"
);

render(<WalletConnection />);

const connectButton = screen.getByRole("button", { name: /connect/i });
userEvent.click(connectButton);

await waitForElementToBeRemoved(connectButton);

// Wait for sync
await screen.findByText(
/account: 0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf/i
);
// Rest of the state should be present
expect(screen.getByText(/chain id: 0x1/i)).toBeInTheDocument();
expect(screen.getByText(/balance: 2.00/i)).toBeInTheDocument();
});

test("User should be able to see a changed account or network", async () => {
// Start with a connected wallet
testingUtils.mockConnectedWallet(
["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"],
{
balance: "0x1bc16d674ec80000",
}
);

render(<WalletConnection />);

const connectButton = screen.getByRole("button", { name: /connect/i });
userEvent.click(connectButton);

await waitForElementToBeRemoved(connectButton);

// Wait for sync
await screen.findByText(
/account: 0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf/i
);

// Mock the balance of the new account
testingUtils.mockBalance(
"0x138071e4e810f34265bd833be9c5dd96f01bd8a5",
"0xde0b6b3a7640000"
);

// Simulate a change of account
act(() => {
testingUtils.mockAccountsChanged([
"0x138071e4e810f34265bd833be9c5dd96f01bd8a5",
]);
});

beforeEach(() => {
jest.spyOn(walletConnectProvider, "getWalletConnectProvider").mockReturnValue(testingUtils.getProvider() as unknown as any)
})

afterEach(() => {
testingUtils.clearAllMocks();

// Wait for sync
await screen.findByText(
/account: 0x138071e4e810f34265bd833be9c5dd96f01bd8a5/i
);
expect(screen.getByText(/balance: 1.00/i)).toBeInTheDocument();

// Mock account balance on the new chain
testingUtils.mockBalance(
"0x138071e4e810f34265bd833be9c5dd96f01bd8a5",
"0x4563918244f40000"
);

// Simulate a change of chain
act(() => {
testingUtils.mockChainChanged("0x3");
});

test("User should be able to connect using Wallet Connect", async () => {
// Start with not connected wallet
testingUtils.mockNotConnectedWallet();

testingUtils.mockAccounts(
["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"]
);
testingUtils.mockChainId(1);
testingUtils.mockBlockNumber(1);
testingUtils.mockBalance("0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf", "0x1bc16d674ec80000");

render(<WalletConnection />);

const connectButton = screen.getByRole("button", { name: /connect/i });
userEvent.click(connectButton);

await waitForElementToBeRemoved(connectButton);

// Wait for sync
await screen.findByText(
/account: 0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf/i
);
// Rest of the state should be present
expect(screen.getByText(/chain id: 0x1/i)).toBeInTheDocument();
expect(screen.getByText(/balance: 2.00/i)).toBeInTheDocument();

// Wait for sync
await screen.findByText(/chain id: 0x3/i);
expect(screen.getByText(/balance: 5.00/i)).toBeInTheDocument();
});

test("User should be able to sign a transaction", async () => {
const bobsWallet = ethers.Wallet.createRandom();
// Start with a connected bobsWallet
testingUtils.mockConnectedWallet([bobsWallet.address]);
// mock out a function to call when we request personal sign
let signedMessages: string[] = [];
testingUtils.lowLevel.mockRequest("personal_sign", async (params: any) => {
let statement = (params as string[])[0];
const signedMessage = await bobsWallet.signMessage(statement);
signedMessages.push(signedMessage);
return signedMessage;
});

test("User should be able to see a changed account or network", async () => {
// Start with a connected wallet
testingUtils.mockConnectedWallet(
["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"],
{
balance: "0x1bc16d674ec80000",
}
);

render(<WalletConnection />);

const connectButton = screen.getByRole("button", { name: /connect/i });
userEvent.click(connectButton);

await waitForElementToBeRemoved(connectButton);

// Wait for sync
await screen.findByText(
/account: 0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf/i
);

// Mock the balance of the new account
testingUtils.mockBalance(
"0x138071e4e810f34265bd833be9c5dd96f01bd8a5",
"0xde0b6b3a7640000"
);

// Simulate a change of account
act(() => {
testingUtils.mockAccountsChanged([
"0x138071e4e810f34265bd833be9c5dd96f01bd8a5",
]);
});

// Wait for sync
await screen.findByText(
/account: 0x138071e4e810f34265bd833be9c5dd96f01bd8a5/i
);
expect(screen.getByText(/balance: 1.00/i)).toBeInTheDocument();

// Mock account balance on the new chain
testingUtils.mockBalance(
"0x138071e4e810f34265bd833be9c5dd96f01bd8a5",
"0x4563918244f40000"
);

// Simulate a change of chain
act(() => {
testingUtils.mockChainChanged("0x3");
});

// Wait for sync
await screen.findByText(/chain id: 0x3/i);
expect(screen.getByText(/balance: 5.00/i)).toBeInTheDocument();
// render the wallet
render(<WalletConnection />);
// connect the wallet
const connectButton = screen.getByRole("button", { name: /connect/i });
userEvent.click(connectButton);
await screen.findByText(`account: ${bobsWallet.address}`, { exact: false });
// get the div for the signature result
const signatureResult = screen.getByTestId("signatureResult");
expect(signatureResult).toHaveTextContent("");
// sign the transaction
const signButton = screen.getByRole("button", { name: /sign/i });
userEvent.click(signButton);
// wait for the result to appear
await waitFor(() => {
expect(signatureResult).not.toHaveTextContent("");
});
// expect it to be the same as the signed message
expect(signatureResult).toHaveTextContent(signedMessages[0]);
});
});
13 changes: 13 additions & 0 deletions examples/react-apps/src/pages/wallet-connect-connection/wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,25 @@ function useWallet() {

function Wallet() {
const { account, chainId, balance } = useWallet();
const [signatureResult, setSignatureResult] = React.useState("");

const personalSign = React.useCallback(async () => {
const provider = getWalletConnectProvider();
const message = "Hello, world!";
const result = await provider.request({
method: "personal_sign",
params: [message, account, ""],
});
setSignatureResult(result);
}, [account]);

return (
<div>
<div>Account: {account}</div>
<div>Chain ID: {chainId}</div>
<div>Balance: {(Number(balance) / 10 ** 18).toFixed(2)}</div>
<div data-testid="signatureResult">{signatureResult}</div>
<button onClick={personalSign}>Sign</button>
</div>
);
}
Expand Down
10 changes: 8 additions & 2 deletions src/mock-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,24 @@ export class MockManager {
/**
* Mock a JSON-RPC request
* @param method JSON-RPC method name
* @param data Data to be resolved, or error to be thrown in case of throw
* @param data Data to be resolved, or function to be called, or error to be thrown in case of throw
* @param mockOptions Options of the mock
* @example ```ts
* // Mock one "eth_accounts" request
* mockManager.mockRequest("eth_accounts", ["0x..."]);
* // Persistently mock "eth_chainId" request
* mockManager.mockRequest("eth_chainId", "0x1", { persistent: true });
* // Mock with a dynamical value based on params
* // "personal_sign" in this case
* mockManager.mockRequest("personal_sign", async (params: any) => {
* let statement = (params as string[])[0];
* return await bobsWallet.signMessage(statement);
* });
* ```
*/
public mockRequest(
method: string,
data: unknown,
data: unknown | ((params: unknown[]) => unknown),
mockOptions: MockOptions = {}
) {
const { condition, persistent } = mockOptions;
Expand Down
Loading

0 comments on commit 613ac0e

Please sign in to comment.