Skip to content

Commit

Permalink
feat: add api spec test infrastructure (#9356)
Browse files Browse the repository at this point in the history
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

This PR adds tests using
[api-specs](https://github.com/MetaMask/api-specs) via the
[@open-rpc/test-coverage](https://github.com/open-rpc/test-coverage)
tool.



<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

## **Related issues**

Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2238

## **Manual testing steps**

1. `yarn setup`
2. `yarn test:e2e:ios:debug:build`
3. `yarn test:api-specs`

## **Screenshots/Recordings**
![Screenshot 2024-06-18 at 3 25
26 PM](https://github.com/MetaMask/metamask-mobile/assets/364566/ecad8a8a-60ed-4f89-b85e-3e4ba34ef692)

![image](https://github.com/MetaMask/metamask-mobile/assets/364566/82e7bfc1-933b-4a14-80ca-ca7baf34904f)

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: Shane Jonas <jonas.shane@gmail.com>
Co-authored-by: Curtis <Curtis.David7@gmail.com>
  • Loading branch information
3 people authored Jun 26, 2024
1 parent 12c15ce commit 94d146e
Show file tree
Hide file tree
Showing 15 changed files with 1,141 additions and 79 deletions.
32 changes: 20 additions & 12 deletions .detoxrc.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
$0: 'jest',
config: 'e2e/jest.e2e.config.js',
},
jest: {
setupTimeout: 220000,
},
retries: 1,
},

artifacts: {
rootDir: "./artifacts/screenshots",
plugins: {
Expand All @@ -23,8 +12,27 @@ module.exports = {
}
},
},
},
},
testRunner: {
args: {
$0: 'jest',
config: 'e2e/jest.e2e.config.js',
},
jest: {
setupTimeout: 220000,
},
retries: 1,
},
configurations: {
'ios.sim.apiSpecs': {
device: 'ios.simulator',
app: 'ios.debug',
testRunner: {
args: {
"$0": "node e2e/api-specs/run-api-spec-tests.js",
},
},
},
'ios.sim.debug': {
device: 'ios.simulator',
app: 'ios.debug',
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,6 @@ app/lib/ppom/ppom.html.js

# ccache
ccache

# open-rpc/test-coverage
html-report/
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { selectChainId } from '../../../../../selectors/networkController';
import ApproveTransactionHeader from '../ApproveTransactionHeader';
import { getActiveTabUrl } from '../../../../../util/transactions';
import { isEqual } from 'lodash';
import { SigningModalSelectorsIDs } from '../../../../../../e2e/selectors/Modals/SigningModal.selectors';
import { AssetWatcherSelectorsIDs } from '../../../../../../e2e/selectors/Modals/AssetWatcher.selectors';
import { getDecimalChainId } from '../../../../../util/networks';
import { useMetrics } from '../../../../../components/hooks/useMetrics';

Expand Down Expand Up @@ -169,8 +169,8 @@ const WatchAssetRequest = ({
</Text>
</View>
<ActionView
cancelTestID={SigningModalSelectorsIDs.CANCEL_BUTTON}
confirmTestID={SigningModalSelectorsIDs.SIGN_BUTTON}
cancelTestID={AssetWatcherSelectorsIDs.CANCEL_BUTTON}
confirmTestID={AssetWatcherSelectorsIDs.CONFIRM_BUTTON}
cancelText={strings('watch_asset_request.cancel')}
confirmText={strings('watch_asset_request.add')}
onCancelPress={onCancel}
Expand Down
70 changes: 70 additions & 0 deletions bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ stages:
- android_e2e_build: {}
run_smoke_e2e_ios_android_stage:
workflows:
- run_ios_api_specs: {}
- run_tag_smoke_accounts_ios: {}
- run_tag_smoke_accounts_android: {}
- run_tag_smoke_assets_ios: {}
Expand Down Expand Up @@ -522,6 +523,9 @@ workflows:
- TEST_SUITE_TAG: '.*SmokeSwaps.*'
after_run:
- android_e2e_test
run_ios_api_specs:
after_run:
- ios_api_specs
run_tag_smoke_core_ios:
envs:
- TEST_SUITE_FOLDER: './e2e/spec/*/**/*'
Expand Down Expand Up @@ -716,6 +720,72 @@ workflows:
bitrise.io:
machine_type_id: elite-xl
stack: linux-docker-android-20.04
ios_api_specs:
before_run:
- setup
- prep_environment
after_run:
- notify_failure
steps:
- pull-intermediate-files@1:
inputs:
- artifact_sources: .*
title: Pull iOS build
- script@1:
title: Copy iOS build for Detox
inputs:
- content: |-
#!/usr/bin/env bash
set -ex
# Create directories for Detox
mkdir -p "$BITRISE_SOURCE_DIR/ios/build/Build"
mkdir -p "$BITRISE_SOURCE_DIR/../Library/Detox/ios"
# Copy saved files for Detox usage
# INTERMEDIATE_IOS_BUILD_DIR & INTERMEDIATE_IOS_DETOX_DIR are the cached directories by ios_e2e_build's "Save iOS build" step
cp -r "$INTERMEDIATE_IOS_BUILD_DIR" "$BITRISE_SOURCE_DIR/ios/build"
cp -r "$INTERMEDIATE_IOS_DETOX_DIR" "$BITRISE_SOURCE_DIR/../Library/Detox"
- restore-cocoapods-cache@2: {}
- restore-cache@2:
title: Restore cache node_modules
inputs:
- key: node_modules-{{ .OS }}-{{ .Arch }}-{{ .CommitHash }}
- certificate-and-profile-installer@1: {}
- set-xcode-build-number@1:
inputs:
- build_short_version_string: $VERSION_NAME
- plist_path: $PROJECT_LOCATION_IOS/MetaMask/Info.plist
- script:
inputs:
- content: |-
# Add cache directory to environment variable
envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils"
envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils"
brew tap wix/brew
title: Set Env Path for caching deps
- script@1:
title: Run detox test
timeout: 1200
is_always_run: false
inputs:
- content: |-
#!/usr/bin/env bash
yarn test:api-specs
- script@1:
is_always_run: true
is_skippable: false
title: Add tests reports to Bitrise
inputs:
- content: |-
#!/usr/bin/env bash
cp -r $BITRISE_SOURCE_DIR/html-report/index.html $BITRISE_HTML_REPORT_DIR/
- deploy-to-bitrise-io@2.2.3:
is_always_run: true
is_skippable: false
inputs:
- deploy_path: $BITRISE_HTML_REPORT_DIR
title: Deploy test report files
ios_e2e_build:
before_run:
- code_setup
Expand Down
187 changes: 187 additions & 0 deletions e2e/api-specs/ConfirmationsRejectionRule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { device } from 'detox';
import { addToQueue } from './helpers';
import paramsToObj from '@open-rpc/test-coverage/build/utils/params-to-obj';
import TestHelpers from '../helpers';
import Matchers from '../utils/Matchers';
import Gestures from '../utils/Gestures';
import ConnectModal from '../pages/modals/ConnectModal';
import AssetWatchModal from '../pages/modals/AssetWatchModal';

// eslint-disable-next-line import/no-nodejs-modules
import fs from 'fs';

import Assertions from '../utils/Assertions';

const getBase64FromPath = async (path) => {
const data = await fs.promises.readFile(path);
return data.toString('base64');
};

export default class ConfirmationsRejectRule {
constructor(options) {
this.driver = options.driver; // Pass element for detox instead of all the driver
this.only = options.only;
this.allCapsCancel = ['wallet_watchAsset'];
this.requiresEthAccountsPermission = [
'personal_sign',
'eth_signTypedData_v4',
'eth_getEncryptionPublicKey',
];
}

getTitle() {
return 'Confirmations Rejection Rule';
}

async beforeRequest(_, call) {
await new Promise((resolve, reject) => {
addToQueue({
name: 'beforeRequest',
resolve,
reject,
task: async () => {
if (this.requiresEthAccountsPermission.includes(call.methodName)) {
const requestPermissionsRequest = JSON.stringify({
jsonrpc: '2.0',
method: 'wallet_requestPermissions',
params: [{ eth_accounts: {} }],
});

await this.driver.runScript(`(el) => {
window.ethereum.request(${requestPermissionsRequest})
}`);

/**
*
* Screenshot Code Section
*
*/

// Connect accounts modal
await Assertions.checkIfVisible(ConnectModal.container);
await ConnectModal.tapConnectButton();
await Assertions.checkIfNotVisible(ConnectModal.container);
await TestHelpers.delay(3000);
}

// we need this because mobile doesnt support just raw json signTypedData, it requires a stringified version
// it was fixed and should get it in @metamask/message-manager@7.0.1
if (call.methodName === 'eth_signTypedData_v4') {
call.params[1] = JSON.stringify(call.params[1]);
}
},
});
});
}

// get all the confirmation calls to make and expect to pass
// Need this now?
getCalls(_, method) {
const calls = [];
const isMethodAllowed = this.only ? this.only.includes(method.name) : true;
if (isMethodAllowed) {
if (method.examples) {
// pull the first example
const e = method.examples[0];
const ex = e;

if (!ex.result) {
return calls;
}
const p = ex.params.map((e) => e.value);
const params =
method.paramStructure === 'by-name'
? paramsToObj(p, method.params)
: p;
calls.push({
title: `${this.getTitle()} - with example ${ex.name}`,
methodName: method.name,
params,
url: '',
resultSchema: method.result.schema,
expectedResult: ex.result.value,
});
} else {
// naively call the method with no params
calls.push({
title: `${method.name} > confirmation rejection`,
methodName: method.name,
params: [],
url: '',
resultSchema: method.result.schema,
});
}
}
return calls;
}

async afterRequest(_, call) {
await new Promise((resolve, reject) => {
addToQueue({
name: 'afterRequest',
resolve,
reject,
task: async () => {
await TestHelpers.delay(3000);
const imagePath = await device.takeScreenshot(
`afterRequest-${this.getTitle()}`,
);
const image = await getBase64FromPath(imagePath);
call.attachments = call.attachments || [];
call.attachments.push({
data: `data:image/png;base64,${image}`,
image,
type: 'image',
});
let cancelButton;
await TestHelpers.delay(3000);
if (this.allCapsCancel.includes(call.methodName)) {
await AssetWatchModal.tapCancelButton();
} else {
cancelButton = await Matchers.getElementByText('Cancel');
await Gestures.waitAndTap(cancelButton);
}
},
});
});

/**
*
* Screen shot code section
*/
}

async afterResponse(_, call) {
await new Promise((resolve, reject) => {
addToQueue({
name: 'afterResponse',
resolve,
reject,
task: async () => {
// Revoke Permissions
if (this.requiresEthAccountsPermission.includes(call.methodName)) {
const revokePermissionRequest = JSON.stringify({
jsonrpc: '2.0',
method: 'wallet_revokePermissions',
params: [{ eth_accounts: {} }],
});

await this.driver.runScript(`(el) => {
window.ethereum.request(${revokePermissionRequest})
}`);
}
},
});
});
}

validateCall(call) {
if (call.error) {
call.valid = call.error.code === 4001;
if (!call.valid) {
call.reason = `Expected error code 4001, got ${call.error.code}`;
}
}
return call;
}
}
Loading

0 comments on commit 94d146e

Please sign in to comment.