Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/guidelines/E2E_DECISION_TREE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ flowchart TD
GR -->|PR label: pr-not-ready-for-e2e| L2[No E2E]
L2 -->|ignorable-only changes| NoBlock[No merge block]
L2 -->|non-ignorable changes| Skip2[Merge blocked]
GR -->|PR ignorable-only changes| Ignorable[No E2E - merge not blocked]
GR -->|PR ignorable-only changes| Ignorable[No E2E]
GR -->|PR has Android-only changes| Android[Android Build + Tests needed]
GR -->|PR has iOS-only changes| iOS[iOS Build + Test needed]
GR -->|PR other files changed| Both[Both Build + Tests needed]
Expand Down
16 changes: 7 additions & 9 deletions .github/scripts/e2e-report-fixture-validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ function buildComment(results) {
}

if (!results) {
if (VALIDATION_RESULT === 'success') {
return `✅ ${COMMENT_MARKER} — Passed**\n[View details](${RUN_URL})`;
}
return null;
}

Expand All @@ -66,11 +63,7 @@ function buildComment(results) {
].join('\n');
}

if (valueMismatches > 0) {
return `✅ ${COMMENT_MARKER} — Schema is up to date**\n${valueMismatches} value mismatches detected (expected — fixture represents an existing user).\n[View details](${RUN_URL})`;
}

return `✅ ${COMMENT_MARKER} — No differences found**\nFixture is up to date. [View details](${RUN_URL})`;
return null;
}

function emitAnnotation(results) {
Expand Down Expand Up @@ -148,11 +141,16 @@ async function main() {

// Post PR comment if this is a PR
if (PR_NUMBER) {
// Always clean up any prior fixture-validation comments so passing runs
// remove stale "structural changes" comments from previous failures.
await deletePreviousComments();

const comment = buildComment(results);
if (comment) {
await deletePreviousComments();
await postComment(comment);
console.log(`Posted fixture validation comment on PR #${PR_NUMBER}`);
} else {
console.log(`No actionable fixture validation findings for PR #${PR_NUMBER} — skipping comment.`);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@ jobs:
ios-tests-ready:
name: 'iOS Tests Ready'
runs-on: ubuntu-latest
if: ${{ needs.build-ios-apps.result == 'success' }}
if: ${{ !cancelled() && needs.build-ios-apps.result == 'success' }}
needs: [build-ios-apps]
steps:
- name: iOS build complete
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/get-requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ jobs:
SHOULD_SKIP_E2E: ${{ steps.skip-e2e-tag.outputs.SKIP == 'true' || steps.check-labels.outputs.SKIP_E2E == 'true' }}
LABEL_BLOCKS_MERGE: ${{ steps.check-labels.outputs.LABEL_BLOCKS_MERGE }}
ALL_CHANGES_COUNT: ${{ steps.filter.outputs.all_changes_count }}
ALL_CHANGES_FILES: ${{ steps.filter.outputs.all_changes_files }}
ALL_CHANGES_FILES: ${{ github.event_name == 'pull_request' && steps.filter.outputs.all_changes_files || '' }}
IGNORABLE_COUNT: ${{ steps.filter.outputs.e2e_ignorable_count }}
E2E_WORKFLOWS_COUNT: ${{ steps.filter.outputs.e2e_relevant_workflows_count }}
ANDROID_COUNT: ${{ steps.filter.outputs.android_count }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ describe('MainActionButton', () => {
/>,
);

expect(getByTestId(MAINACTIONBUTTON_TEST_ID).props.style).toContainEqual(
expect.objectContaining(customStyle),
expect(getByTestId(MAINACTIONBUTTON_TEST_ID).props.style).toMatchObject(
customStyle,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

// Third party dependencies.
import React from 'react';
import { Pressable, View, Animated } from 'react-native';
import { View, Animated, Pressable } from 'react-native';

// External dependencies.
import Icon, { IconSize, IconColor } from '../../components/Icons/Icon';
Expand All @@ -26,7 +26,9 @@ const MainActionButton = ({
onPressIn,
onPressOut,
style,
containerStyle,
isDisabled = false,
testID,
...props
}: MainActionButtonProps) => {
const { styles } = useStyles(styleSheet, {
Expand All @@ -40,13 +42,16 @@ const MainActionButton = ({
});

return (
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
<Animated.View
style={[{ transform: [{ scale: scaleAnim }] }, containerStyle]}
>
<Pressable
style={({ pressed }) => [styles.base, pressed && styles.pressed]}
testID={testID}
accessible
style={styles.base}
onPress={!isDisabled ? onPress : undefined}
onPressIn={!isDisabled ? handlePressIn : undefined}
onPressOut={!isDisabled ? handlePressOut : undefined}
accessible
disabled={isDisabled}
{...props}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Third party dependencies.
import { PressableProps } from 'react-native';
import { PressableProps, StyleProp, ViewStyle } from 'react-native';

// External dependencies.
import { IconName } from '../../components/Icons/Icon';
Expand All @@ -8,6 +8,11 @@ import { IconName } from '../../components/Icons/Icon';
* MainActionButton component props.
*/
export interface MainActionButtonProps extends PressableProps {
/**
* Optional style applied to the outermost Animated.View container.
* Use this to control layout (e.g. flex: 1) without adding a wrapper node.
*/
containerStyle?: StyleProp<ViewStyle>;
/**
* Icon name of the icon that will be displayed.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jest.mock('@metamask/design-system-twrnc-preset', () => ({
useTailwind: () => ({ style: () => ({}) }),
}));

// ButtonIcon's built-in testID from the design system
const BUTTON_ICON_TEST_ID = 'button-icon';
// Arrow icon testID
const ARROW_ICON_TEST_ID = 'section-header-arrow-icon';
const CONTAINER_TEST_ID = 'section-header-container';

describe('SectionHeader', () => {
Expand Down Expand Up @@ -53,15 +53,15 @@ describe('SectionHeader', () => {
it('does not render trailing icon when onPress is not provided', () => {
const { queryByTestId } = render(<SectionHeader title="Tokens" />);

expect(queryByTestId(BUTTON_ICON_TEST_ID)).not.toBeOnTheScreen();
expect(queryByTestId(ARROW_ICON_TEST_ID)).not.toBeOnTheScreen();
});

it('renders trailing icon when onPress is provided', () => {
const { getByTestId } = render(
<SectionHeader title="Tokens" onPress={jest.fn()} />,
);

expect(getByTestId(BUTTON_ICON_TEST_ID)).toBeOnTheScreen();
expect(getByTestId(ARROW_ICON_TEST_ID)).toBeOnTheScreen();
});

it('has button accessibilityRole when onPress is provided', () => {
Expand Down Expand Up @@ -136,15 +136,15 @@ describe('SectionHeader', () => {
/>,
);

expect(getByTestId(BUTTON_ICON_TEST_ID)).toBeOnTheScreen();
expect(getByTestId(ARROW_ICON_TEST_ID)).toBeOnTheScreen();
});

it('does not render the trailing icon when onPress is absent', () => {
const { queryByTestId } = render(
<SectionHeader title="NFTs" endIconName={IconName.Arrow2Right} />,
);

expect(queryByTestId(BUTTON_ICON_TEST_ID)).not.toBeOnTheScreen();
expect(queryByTestId(ARROW_ICON_TEST_ID)).not.toBeOnTheScreen();
});
});

Expand All @@ -169,7 +169,7 @@ describe('SectionHeader', () => {
<SectionHeader title="Tokens" onPress={jest.fn()} />,
);

expect(getByTestId(BUTTON_ICON_TEST_ID)).toBeOnTheScreen();
expect(getByTestId(ARROW_ICON_TEST_ID)).toBeOnTheScreen();
});

it('renders the trailing icon with a custom color', () => {
Expand All @@ -181,7 +181,7 @@ describe('SectionHeader', () => {
/>,
);

expect(getByTestId(BUTTON_ICON_TEST_ID)).toBeOnTheScreen();
expect(getByTestId(ARROW_ICON_TEST_ID)).toBeOnTheScreen();
});
});

Expand All @@ -199,7 +199,7 @@ describe('SectionHeader', () => {

expect(getByText('Tokens')).toBeOnTheScreen();
expect(getByText('Badge')).toBeOnTheScreen();
expect(getByTestId(BUTTON_ICON_TEST_ID)).toBeOnTheScreen();
expect(getByTestId(ARROW_ICON_TEST_ID)).toBeOnTheScreen();
expect(getByTestId(CONTAINER_TEST_ID)).toBeOnTheScreen();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import { StyleSheet, TouchableOpacity, View } from 'react-native';

// External dependencies.
import {
Box,
Text,
ButtonIcon,
Icon,
IconName,
ButtonIconSize,
IconSize,
TextVariant,
BoxFlexDirection,
BoxAlignItems,
TextColor,
IconColor,
} from '@metamask/design-system-react-native';
Expand Down Expand Up @@ -55,20 +52,18 @@ const SectionHeader: React.FC<SectionHeaderProps> = ({
}) => {
const tw = useTailwind();

// Default horizontal padding; apply style to this same container so callers can override padding (e.g. paddingHorizontal: 0)
const containerTwClassName = twClassName ? `px-4 ${twClassName}` : 'px-4';
// Default horizontal padding + row layout; apply style to this same container so callers can override padding (e.g. paddingHorizontal: 0)
const containerTwClassName = twClassName
? `px-4 flex-row items-center ${twClassName}`
: 'px-4 flex-row items-center';
const containerStyle = StyleSheet.flatten([
tw.style(containerTwClassName),
justifyContent ? tw.style(justifyContent) : undefined,
style,
]);

const content = (
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
justifyContent={justifyContent}
twClassName="flex-1"
>
const innerContent = (
<>
{typeof title === 'string' ? (
<Text variant={TextVariant.HeadingMd} color={TextColor.TextDefault}>
{title}
Expand All @@ -77,40 +72,38 @@ const SectionHeader: React.FC<SectionHeaderProps> = ({
title
)}

{/* Arrow icon: 4px right of title, visual indicator only */}
{/* Arrow icon: visual indicator only, no touch handling */}
{onPress && (
<View pointerEvents="none" style={tw.style('ml-1')}>
<ButtonIcon
iconName={endIconName}
size={ButtonIconSize.Sm}
iconProps={{
color: endIconColor,
}}
/>
</View>
<Icon
testID="section-header-arrow-icon"
name={endIconName}
size={IconSize.Sm}
color={endIconColor}
style={tw.style('ml-1')}
/>
)}

{endAccessory}
</Box>
</>
);

if (onPress) {
return (
<TouchableOpacity
testID={testID}
onPress={onPress}
style={containerStyle}
accessibilityRole="button"
accessibilityLabel={typeof title === 'string' ? title : undefined}
style={containerStyle}
>
{content}
{innerContent}
</TouchableOpacity>
);
}

return (
<View testID={testID} style={containerStyle}>
{content}
{innerContent}
</View>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,7 @@ describe('TabBar', () => {

fireEvent.press(getByTestId(`tab-bar-item-${TabBarIconKey.Wallet}`));
expect(navigation.navigate).toHaveBeenCalledWith(Routes.WALLET.HOME, {
screen: Routes.WALLET.TAB_STACK_FLOW,
params: {
screen: Routes.WALLET_VIEW,
},
screen: Routes.WALLET_VIEW,
});

fireEvent.press(getByTestId(`tab-bar-item-${TabBarIconKey.Browser}`));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,7 @@ const TabBar = ({ state, descriptors, navigation }: TabBarProps) => {
switch (rootScreenName) {
case Routes.WALLET_VIEW:
navigation.navigate(Routes.WALLET.HOME, {
screen: Routes.WALLET.TAB_STACK_FLOW,
params: {
screen: Routes.WALLET_VIEW,
},
screen: Routes.WALLET_VIEW,
});
break;
case Routes.MODAL.WALLET_ACTIONS:
Expand Down
1 change: 1 addition & 0 deletions app/components/Base/RemoteImage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const RemoteImage: React.FC<RemoteImageProps> = (props) => {
recyclingKey={uri}
onLoad={onImageLoad}
onError={onError}
accessible={false}
/>
);

Expand Down
38 changes: 28 additions & 10 deletions app/components/Base/TokenIcon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import {
Image,
ImageSourcePropType,
ImageStyle,
StyleSheet,
TextStyle,
View,
Expand Down Expand Up @@ -166,20 +168,36 @@ function TokenIcon({
const source = getSource();

if (source && !showFallback) {
const iconStyle = [
styles.icon,
medium && styles.iconMedium,
big && styles.iconBig,
biggest && styles.iconBiggest,
style,
];
// Use standard RN Image for local bundled assets (BTC, ETH, SOL, etc.)
// expo-image (via RemoteImage) interferes with iOS accessibility tree,
// preventing the parent TouchableOpacity from being detected by XCUITest.
// Only use RemoteImage for remote URL sources (when `icon` prop is a URL).
const isRemoteUrl = typeof source === 'object' && 'uri' in source;
if (isRemoteUrl) {
return (
<RemoteImage
key={icon || `symbol-${symbol}`}
testID={testID}
source={getSource()}
onError={() => setShowFallback(true)}
style={iconStyle}
/>
);
}
return (
<RemoteImage
<Image
key={icon || `symbol-${symbol}`}
testID={testID}
fadeIn
source={getSource()}
source={getSource() as ImageSourcePropType}
onError={() => setShowFallback(true)}
style={[
styles.icon,
medium && styles.iconMedium,
big && styles.iconBig,
biggest && styles.iconBiggest,
style,
]}
style={iconStyle as ImageStyle[]}
/>
);
}
Expand Down
Loading
Loading