From 6a77e4b461e47d2626632474a01e2e7bd1a92f44 Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Thu, 24 Oct 2024 17:49:03 +0200 Subject: [PATCH 1/2] fix: prevent Duplicate Block Explorer Entries and Ensure Proper Input Validation (#11995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** When editing a network and attempting to add a new block explorer URL, the 'Add Block Explorer URL +' button allows users to add a duplicate entry even if no input is provided. This results in duplicate block explorer URLs being added to the list. The expected behavior is that the 'Add Block Explorer URL' button should remain disabled until valid input is provided, and existing URLs should not be re-added. ## **Related issues** Fixes: #11990 ## **Manual testing steps** 1. Go to network add form 2. go to add block explorer url 3. The 'Add Block Explorer URL' button should be disabled until a valid URL is inputted into the field. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/05f72bf8-8046-40a4-a941-51f39cfc9a09 ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile 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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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. --- .../NetworksSettings/NetworkSettings/index.js | 43 +- .../NetworkSettings/index.test.tsx | 460 +++++++++++++++++- 2 files changed, 494 insertions(+), 9 deletions(-) diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js index 5f70f739948..17e68d679a7 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js @@ -447,6 +447,7 @@ export class NetworkSettings extends PureComponent { blockExplorerUrls: [], selectedRpcEndpointIndex: 0, blockExplorerUrl: undefined, + blockExplorerUrlForm: undefined, nickname: undefined, chainId: undefined, ticker: undefined, @@ -714,7 +715,11 @@ export class NetworkSettings extends PureComponent { // in an error message in the form. if (!formChainId.startsWith('0x')) { try { - endpointChainId = new BigNumber(endpointChainId, 16).toString(10); + const endpointChainIdNumber = new BigNumber(endpointChainId, 16); + if (endpointChainIdNumber.isNaN()) { + throw new Error('Invalid endpointChainId'); + } + endpointChainId = endpointChainIdNumber.toString(10); } catch (err) { Logger.error(err, { endpointChainId, @@ -1288,10 +1293,21 @@ export class NetworkSettings extends PureComponent { }; onBlockExplorerItemAdd = async (url) => { + // If URL is empty or undefined, return early if (!url) { return; } + // Check if the URL already exists in blockExplorerUrls + const { blockExplorerUrls } = this.state; + const urlExists = blockExplorerUrls.includes(url); + + if (urlExists) { + // If the URL already exists, return early + return; + } + + // If the URL doesn't exist, proceed with adding it await this.setState((prevState) => ({ blockExplorerUrls: [...prevState.blockExplorerUrls, url], })); @@ -1351,6 +1367,7 @@ export class NetworkSettings extends PureComponent { onBlockExplorerUrlChange = async (url) => { const { addMode } = this.state; await this.setState({ + blockExplorerUrlForm: url, blockExplorerUrl: url, }); @@ -1486,7 +1503,10 @@ export class NetworkSettings extends PureComponent { }; closeAddBlockExplorerRpcForm = () => { - this.setState({ showAddBlockExplorerForm: { isVisible: false } }); + this.setState({ + showAddBlockExplorerForm: { isVisible: false }, + blockExplorerUrlForm: undefined, + }); }; closeRpcModal = () => { @@ -1603,6 +1623,7 @@ export class NetworkSettings extends PureComponent { rpcUrlForm, rpcNameForm, rpcName, + blockExplorerUrlForm, } = this.state; const { route, networkConfigurations } = this.props; const isCustomMainnet = route.params?.isCustomMainnet; @@ -2204,6 +2225,7 @@ export class NetworkSettings extends PureComponent { ref={this.inputBlockExplorerURL} style={inputStyle} autoCapitalize={'none'} + value={blockExplorerUrlForm} autoCorrect={false} onChangeText={this.onBlockExplorerUrlChange} placeholder={strings( @@ -2214,23 +2236,28 @@ export class NetworkSettings extends PureComponent { onSubmitEditing={this.toggleNetworkDetailsModal} keyboardAppearance={themeAppearance} /> - {blockExplorerUrl && !isUrl(blockExplorerUrl) && ( - + {blockExplorerUrl && + (!isUrl(blockExplorerUrl) || + blockExplorerUrls.includes(blockExplorerUrlForm)) && ( {strings('app_settings.invalid_block_explorer_url')} - - )} + )} + { - this.onBlockExplorerItemAdd(blockExplorerUrl); + this.onBlockExplorerItemAdd(blockExplorerUrlForm); }} width={ButtonWidthTypes.Full} labelTextVariant={TextVariant.DisplayMD} - isDisabled={!blockExplorerUrl || !isUrl(blockExplorerUrl)} + isDisabled={ + !blockExplorerUrl || + !blockExplorerUrlForm || + !isUrl(blockExplorerUrl) + } /> diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx index 1ecffde7034..7f55c1d687c 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx @@ -8,6 +8,9 @@ import { ThemeContext, mockTheme } from '../../../../../../app/util/theme'; import { backgroundState } from '../../../../../util/test/initial-root-state'; import { isNetworkUiRedesignEnabled } from '../../../../../util/networks/isNetworkUiRedesignEnabled'; import { mockNetworkState } from '../../../../../util/test/network'; +// eslint-disable-next-line import/no-namespace +import * as jsonRequest from '../../../../../util/jsonRpcRequest'; +import Logger from '../../../../../util/Logger'; import Engine from '../../../../../core/Engine'; // Mock the entire module @@ -87,6 +90,7 @@ const SAMPLE_NETWORKSETTINGS_PROPS = { rpcEndpoints: [{ url: 'https://goerli.infura.io/v3/{infuraProjectId}' }], }, }, + networkOnboardedState: { '0x1': true, '0xe708': true }, navigation: { setOptions: jest.fn(), navigate: jest.fn(), goBack: jest.fn() }, matchedChainNetwork: { safeChainsList: [ @@ -756,6 +760,49 @@ describe('NetworkSettings', () => { expect(wrapper.state('warningRpcUrl')).toBe('Invalid RPC URL'); }); + it('should set a warning if the RPC URL format is invalid', async () => { + const instance = wrapper.instance(); + + await instance.validateRpcUrl('invalidUrl'); + expect(wrapper.state('warningRpcUrl')).toBe( + 'URIs require the appropriate HTTPS prefix', + ); + }); + + it('should set a warning for a duplicated RPC URL', async () => { + const instance = wrapper.instance(); + + await instance.validateRpcUrl( + 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID', + ); + expect(wrapper.state('warningRpcUrl')).toBe('Invalid RPC URL'); + }); + + it('should set a warning if the RPC URL already exists in networkConfigurations and UI redesign is disabled', async () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => false); + const instance = wrapper.instance(); + + await instance.validateRpcUrl( + 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID', + ); + await instance.validateRpcUrl( + 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID', + ); + expect(wrapper.state('warningRpcUrl')).toBe('Invalid RPC URL'); + expect(wrapper.state('validatedRpcURL')).toBe(true); + }); + + it('should set a warning if the RPC URL exists and UI redesign is enabled', async () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + const instance = wrapper.instance(); + + await instance.validateRpcUrl( + 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID', + ); + expect(wrapper.state('warningRpcUrl')).toBe('Invalid RPC URL'); + expect(wrapper.state('validatedRpcURL')).toBe(true); + }); + it('should correctly add RPC URL through modal and update state', async () => { const instance = wrapper.instance(); @@ -781,6 +828,47 @@ describe('NetworkSettings', () => { ); }); + it('should not add an empty Block Explorer URL and should return early', async () => { + const instance = wrapper.instance(); + + // Initially, blockExplorerUrls should be empty + expect(wrapper.state('blockExplorerUrls').length).toBe(0); + + // Open Block Explorer form modal and attempt to add an empty URL + instance.openAddBlockExplorerForm(); + await instance.onBlockExplorerItemAdd(''); + + // Ensure the state is not updated with the empty URL + expect(wrapper.state('blockExplorerUrls').length).toBe(0); + expect(wrapper.state('blockExplorerUrl')).toBeUndefined(); + }); + + it('should not add an existing Block Explorer URL and should return early', async () => { + const instance = wrapper.instance(); + + // Set initial state with an existing block explorer URL + await instance.setState({ + blockExplorerUrls: ['https://existing-blockexplorer.com'], + }); + + // Ensure the initial state contains the existing URL + expect(wrapper.state('blockExplorerUrls').length).toBe(1); + expect(wrapper.state('blockExplorerUrls')[0]).toBe( + 'https://existing-blockexplorer.com', + ); + + // Attempt to add the same URL again + await instance.onBlockExplorerItemAdd( + 'https://existing-blockexplorer.com', + ); + + // Ensure the state remains unchanged and no duplicate is added + expect(wrapper.state('blockExplorerUrls').length).toBe(1); + expect(wrapper.state('blockExplorerUrls')[0]).toBe( + 'https://existing-blockexplorer.com', + ); + }); + it('should call validateRpcAndChainId when chainId and rpcUrl are set', async () => { const instance = wrapper.instance(); const validateRpcAndChainIdSpy = jest.spyOn( @@ -885,7 +973,7 @@ describe('NetworkSettings', () => { it('should handle valid chainId conversion and updating state correctly', async () => { const instance = wrapper.instance(); - await instance.onChainIDChange('0x1'); + await instance.onChainIDChange('0x2'); await instance.validateChainId(); expect(wrapper.state('warningChainId')).toBe(undefined); @@ -1355,4 +1443,374 @@ describe('NetworkSettings', () => { ]); }); }); + + describe('templateInfuraRpc', () => { + it('should not replace anything if {infuraProjectId} is not in endpoint', () => { + const instance = wrapper.instance(); + + const endpoint = 'https://mainnet.infura.io/v3/someOtherId'; + const result = instance.templateInfuraRpc(endpoint); + expect(result).toBe('https://mainnet.infura.io/v3/someOtherId'); + }); + + it('should replace {infuraProjectId} with an empty string if infuraProjectId is undefined', () => { + const instance = wrapper.instance(); + const endpoint = 'https://mainnet.infura.io/v3/{infuraProjectId}'; + const result = instance.templateInfuraRpc(endpoint); + expect(result).toBe('https://mainnet.infura.io/v3/'); + }); + + it('should return the original endpoint if it does not end with {infuraProjectId}', () => { + const instance = wrapper.instance(); + const endpoint = 'https://mainnet.infura.io/v3/anotherProjectId'; + const result = instance.templateInfuraRpc(endpoint); + expect(result).toBe(endpoint); + }); + }); + + describe('validateChainIdOnSubmit', () => { + beforeEach(() => { + // Spying on the methods we want to mock + jest.spyOn(Logger, 'error'); // Spy on Logger.error + jest.spyOn(jsonRequest, 'jsonRpcRequest'); // Spy on jsonRpcRequest directly + }); + afterEach(() => { + jest.resetAllMocks(); // Clean up mocks after each test + }); + + it('should validate chainId when parsedChainId matches endpoint chainId', async () => { + const instance = wrapper.instance(); + + (jsonRequest.jsonRpcRequest as jest.Mock).mockResolvedValue('0x38'); + + const validChainId = '0x38'; + const rpcUrl = 'https://bsc-dataseed.binance.org/'; + + await instance.validateChainIdOnSubmit( + validChainId, + validChainId, + rpcUrl, + ); + + expect(instance.state.warningChainId).toBeUndefined(); + expect(jsonRequest.jsonRpcRequest).toHaveBeenCalledWith( + 'https://bsc-dataseed.binance.org/', + 'eth_chainId', + ); + }); + + it('should set a warning when chainId is invalid (RPC error)', async () => { + const instance = wrapper.instance(); + + (jsonRequest.jsonRpcRequest as jest.Mock).mockRejectedValue( + new Error('RPC error'), + ); + + const invalidChainId = '0xInvalidChainId'; + const rpcUrl = 'https://bsc-dataseed.binance.org/'; + + await instance.validateChainIdOnSubmit( + invalidChainId, + invalidChainId, + rpcUrl, + ); + + expect(instance.state.warningChainId).toBe( + 'Could not fetch chain ID. Is your RPC URL correct?', + ); + expect(Logger.error).toHaveBeenCalled(); // Ensures the error is logged + }); + + it('should set a warning when parsedChainId does not match endpoint chainId', async () => { + const instance = wrapper.instance(); + + (jsonRequest.jsonRpcRequest as jest.Mock).mockResolvedValue('0x39'); + + const validChainId = '0x38'; + const rpcUrl = 'https://bsc-dataseed.binance.org/'; + + await instance.validateChainIdOnSubmit( + validChainId, + validChainId, + rpcUrl, + ); + + expect(instance.state.warningChainId).toBe( + 'The endpoint returned a different chain ID: 0x39', + ); + }); + + it('should convert endpointChainId to decimal if formChainId is decimal and not hexadecimal', async () => { + const instance = wrapper.instance(); + + (jsonRequest.jsonRpcRequest as jest.Mock).mockResolvedValue('0x38'); + + const decimalChainId = '56'; // Decimal chain ID + const rpcUrl = 'https://bsc-dataseed.binance.org/'; + + await instance.validateChainIdOnSubmit( + decimalChainId, + decimalChainId, + rpcUrl, + ); + + expect(instance.state.warningChainId).toBe( + 'The endpoint returned a different chain ID: 56', + ); + }); + + it('should log error if the conversion from hexadecimal to decimal fails', async () => { + const instance = wrapper.instance(); + + (jsonRequest.jsonRpcRequest as jest.Mock).mockResolvedValue( + '0xInvalidHex', + ); + + const decimalChainId = 'test'; // Invalid decimal chain ID + const rpcUrl = 'https://bsc-dataseed.binance.org/'; + + await instance.validateChainIdOnSubmit( + decimalChainId, + decimalChainId, + rpcUrl, + ); + + expect(Logger.error).toHaveBeenCalledWith(expect.any(Error), { + endpointChainId: '0xInvalidHex', + message: 'Failed to convert endpoint chain ID to decimal', + }); + }); + }); + + describe('addRpcUrl', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let instance: any; + + beforeEach(() => { + instance = wrapper.instance(); + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + + // Mocking dependent methods + jest.spyOn(instance, 'disabledByChainId').mockReturnValue(false); + jest.spyOn(instance, 'disabledBySymbol').mockReturnValue(false); + jest + .spyOn(instance, 'checkIfNetworkNotExistsByChainId') + .mockResolvedValue([]); + jest.spyOn(instance, 'checkIfNetworkExists').mockResolvedValue(false); + jest.spyOn(instance, 'validateChainIdOnSubmit').mockResolvedValue(true); + jest.spyOn(instance, 'handleNetworkUpdate').mockResolvedValue({}); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should add RPC URL correctly', async () => { + wrapper.setState({ + rpcUrl: 'http://localhost:8545', + chainId: '0x1', + ticker: 'ETH', + nickname: 'Localhost', + enableAction: true, + addMode: true, + editable: false, + }); + + await instance.addRpcUrl(); + + expect(instance.handleNetworkUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + rpcUrl: 'http://localhost:8545', + chainId: '0x1', + ticker: 'ETH', + nickname: 'Localhost', + }), + ); + }); + + it('should return early if CTA is disabled by enableAction', async () => { + wrapper.setState({ enableAction: false }); + + await instance.addRpcUrl(); + + expect(instance.handleNetworkUpdate).not.toHaveBeenCalled(); + }); + + it('should return early if CTA is disabled by chainId', async () => { + instance.disabledByChainId.mockReturnValue(true); + + await instance.addRpcUrl(); + + expect(instance.handleNetworkUpdate).not.toHaveBeenCalled(); + }); + + it('should return early if CTA is disabled by symbol', async () => { + instance.disabledBySymbol.mockReturnValue(true); + + await instance.addRpcUrl(); + + expect(instance.handleNetworkUpdate).not.toHaveBeenCalled(); + }); + + it('should not proceed if validateChainIdOnSubmit fails', async () => { + instance.validateChainIdOnSubmit.mockResolvedValue(false); + + await instance.addRpcUrl(); + + expect(instance.handleNetworkUpdate).not.toHaveBeenCalled(); + }); + + it('should check if network already exists in add mode', async () => { + wrapper.setState({ addMode: true, chainId: '0x1', enableAction: true }); + + await instance.addRpcUrl(); + + expect(instance.checkIfNetworkNotExistsByChainId).toHaveBeenCalledWith( + '0x1', + ); + expect(instance.checkIfNetworkExists).not.toHaveBeenCalled(); + }); + + it('should check if network exists in edit mode', async () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => false); + + wrapper.setState({ + chainId: '0x1', + editable: false, + rpcUrl: 'http://localhost:8545', + enableAction: true, + }); + + await instance.addRpcUrl(); + + expect(instance.checkIfNetworkExists).toHaveBeenCalledWith( + 'http://localhost:8545', + ); + expect(instance.checkIfNetworkNotExistsByChainId).not.toHaveBeenCalled(); + }); + + it('should handle custom mainnet condition', async () => { + wrapper.setProps({ + route: { + params: { + isCustomMainnet: true, + }, + }, + }); + + wrapper.setState({ + rpcUrl: 'http://localhost:8545', + chainId: '0x1', + ticker: 'ETH', + nickname: 'Localhost', + enableAction: true, + addMode: true, + editable: false, + }); + + await instance.addRpcUrl(); + + expect(instance.handleNetworkUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + isCustomMainnet: true, + showNetworkOnboarding: false, + }), + ); + }); + + it('should handle network switch pop to wallet condition', async () => { + wrapper.setProps({ + route: { + params: { + shouldNetworkSwitchPopToWallet: false, + }, + }, + }); + + wrapper.setState({ + rpcUrl: 'http://localhost:8545', + chainId: '0x1', + ticker: 'ETH', + nickname: 'Localhost', + enableAction: true, + addMode: true, + editable: false, + }); + + await instance.addRpcUrl(); + + expect(instance.handleNetworkUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + shouldNetworkSwitchPopToWallet: false, + }), + ); + }); + }); + + describe('checkIfNetworkExists', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let instance: any; + + beforeEach(() => { + instance = wrapper.instance(); + + jest.spyOn(instance, 'setState'); + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear all spies after each test + }); + + it('should return custom network if rpcUrl exists in networkConfigurations and UI redesign is disabled', async () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => false); + + const rpcUrl = 'http://localhost:8545'; + + // Mocking props + wrapper.setProps({ + networkConfigurations: { + customNetwork1: { rpcUrl }, + }, + }); + + const result = await instance.checkIfNetworkExists(rpcUrl); + + expect(result).toEqual([{ rpcUrl }]); + expect(instance.setState).toHaveBeenCalledWith({ + warningRpcUrl: 'This network has already been added.', + }); + }); + + it('should return custom network if rpcUrl exists in networkConfigurations and UI redesign is enabled', async () => { + const rpcUrl = 'http://localhost:8545'; + + // Mocking props and enabling network UI redesign + wrapper.setProps({ + networkConfigurations: { + customNetwork1: { rpcUrl }, + }, + }); + + const result = await instance.checkIfNetworkExists(rpcUrl); + + expect(result).toEqual([{ rpcUrl }]); + expect(instance.setState).not.toHaveBeenCalled(); // Should not set warning when redesign is enabled + }); + + it('should return an empty array if rpcUrl does not exist in any networks', async () => { + const rpcUrl = 'https://nonexistent.rpc.url'; + + // Mocking props + wrapper.setProps({ + networkConfigurations: { + customNetwork1: { rpcUrl: 'http://localhost:8545' }, + }, + }); + + const result = await instance.checkIfNetworkExists(rpcUrl); + + expect(result).toEqual([]); + }); + }); }); From ea176e9c972746f05eb784da1e74e028c71ed44d Mon Sep 17 00:00:00 2001 From: Curtis David Date: Thu, 24 Oct 2024 11:50:05 -0400 Subject: [PATCH 2/2] test: Detox: add video recording on failure (#11987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Right now, Detox does a good job of capturing screenshots when a test fails. But what we really need is the ability to provide engineers with video recordings that show exactly what happened leading up to a failure. This would give a clearer picture, making it easier to diagnose and fix issues by seeing the interactions and events in real-time, rather than trying to piece things together from a single snapshot. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** Recording of test failure on iOS https://github.com/user-attachments/assets/0efe7e3c-2366-4b89-bbaf-4eb4ad7e1cc7 Recording of test failure on android https://github.com/user-attachments/assets/261268aa-dda7-4977-9125-027ed9beb512 ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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: legobeat <109787230+legobeat@users.noreply.github.com> --- .detoxrc.js | 9 +++++++-- bitrise.yml | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.detoxrc.js b/.detoxrc.js index b1a80212b46..46f0b4441e1 100644 --- a/.detoxrc.js +++ b/.detoxrc.js @@ -1,7 +1,7 @@ /** @type {Detox.DetoxConfig} */ module.exports = { artifacts: { - rootDir: "./artifacts/screenshots", + rootDir: "./artifacts", plugins: { screenshot: { shouldTakeAutomaticSnapshots: true, @@ -9,10 +9,15 @@ module.exports = { takeWhen: { testStart: false, testDone: false, - } + }, + }, + video: { + enabled: true, // Enable video recording + keepOnlyFailedTestsArtifacts: true, // Keep only failed tests' videos }, }, }, + testRunner: { args: { $0: 'jest', diff --git a/bitrise.yml b/bitrise.yml index 070b13b5846..3b47f2039d6 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -746,7 +746,7 @@ workflows: - content: |- #!/usr/bin/env bash set -ex - cp -r "$BITRISE_SOURCE_DIR/artifacts/screenshots" "$BITRISE_DEPLOY_DIR" + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" - deploy-to-bitrise-io@2.3: title: Deploy test screenshots is_always_run: true @@ -754,7 +754,7 @@ workflows: inputs: - deploy_path: $BITRISE_DEPLOY_DIR - is_compress: true - - zip_name: E2E_Android_Failure_Screenshots + - zip_name: E2E_Android_Failure_Artifacts meta: bitrise.io: machine_type_id: elite-xl @@ -1004,7 +1004,7 @@ workflows: - content: |- #!/usr/bin/env bash set -ex - cp -r "$BITRISE_SOURCE_DIR/artifacts/screenshots" "$BITRISE_DEPLOY_DIR" + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" - deploy-to-bitrise-io@2.3: is_always_run: true run_if: .IsBuildFailed @@ -1012,7 +1012,7 @@ workflows: inputs: - deploy_path: $BITRISE_DEPLOY_DIR - is_compress: true - - zip_name: 'E2E_IOS_Failure_Screenshots' + - zip_name: 'E2E_IOS_Failure_Artifacts' start_e2e_tests: steps: - build-router-start@0: