/*
Copyright 2026 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
or agreed to in writing, software distributed under the License is
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF
ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
*/
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import React from 'react';
import { Linking } from 'react-native';
import { ThemeProvider } from '../../theme/ThemeProvider';
import Button from './Button';
// Mock Linking
jest.spyOn(Linking, 'openURL');
jest.spyOn(Linking, 'canOpenURL');
// Helper to render with theme
const renderWithTheme = (component: React.ReactElement, customThemes?: any) => {
return render(
{component}
);
};
describe('Button', () => {
const mockOnPress = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(Linking.canOpenURL as unknown as jest.Mock).mockResolvedValue(true as any);
(Linking.openURL as unknown as jest.Mock).mockResolvedValue(undefined as any);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Basic rendering', () => {
it('should render a button with the provided title', () => {
render();
const button = screen.getByText('Click Me');
expect(button).toBeTruthy();
});
it('should render with different title text', () => {
render();
expect(screen.getByText('Submit')).toBeTruthy();
});
it('should render button as Pressable component', () => {
const { getByTestId } = render(
);
const button = getByTestId('test-button');
expect(button.type).toBe('View'); // Pressable renders as View
});
});
describe('Press handling', () => {
it('should call onPress when the button is pressed', () => {
render();
const button = screen.getByText('Press Me');
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalledTimes(1);
});
it('should call onPress with interactId when provided', () => {
render(
);
const button = screen.getByText('Press Me');
fireEvent.press(button);
// First argument should be interactId; second (event) may be undefined in tests
expect(mockOnPress).toHaveBeenCalled();
expect((mockOnPress.mock.calls[0] ?? [])[0]).toBe('test-interact-id');
});
it('should call onPress multiple times when pressed multiple times', () => {
render();
const button = screen.getByText('Press Me');
fireEvent.press(button);
fireEvent.press(button);
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalledTimes(3);
});
it('should not throw error when onPress is not provided', () => {
render();
const button = screen.getByText('Press Me');
expect(() => fireEvent.press(button)).not.toThrow();
});
});
describe('actionUrl handling', () => {
it('should open URL when actionUrl is provided and button is pressed', async () => {
const testUrl = 'https://example.com';
render();
const button = screen.getByText('Link');
fireEvent.press(button);
await waitFor(() =>
expect(Linking.openURL).toHaveBeenCalledWith(testUrl)
);
});
it('should call both onPress and open URL when both are provided', async () => {
const testUrl = 'https://example.com';
render(
);
const button = screen.getByText('Link');
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(Linking.openURL).toHaveBeenCalledWith(testUrl)
);
});
it('should call onPress with interactId and open URL', async () => {
const testUrl = 'https://example.com';
render(
);
const button = screen.getByText('Link');
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalled();
expect((mockOnPress.mock.calls[0] ?? [])[0]).toBe('link-interact-id');
await waitFor(() =>
expect(Linking.openURL).toHaveBeenCalledWith(testUrl)
);
});
it('should call openURL even if it might fail', async () => {
const testUrl = 'https://example.com';
render();
const button = screen.getByText('Link');
fireEvent.press(button);
// Verify the URL opening was attempted
await waitFor(() =>
expect(Linking.openURL).toHaveBeenCalledWith(testUrl)
);
});
it('should warn if openURL throws', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const testUrl = 'https://example.com';
(Linking.canOpenURL as unknown as jest.Mock).mockResolvedValueOnce(true as any);
(Linking.openURL as unknown as jest.Mock).mockImplementationOnce(
() => Promise.reject(new Error('open failed')) as any
);
render();
const button = screen.getByText('Link');
expect(() => fireEvent.press(button)).not.toThrow();
await waitFor(() =>
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(`Failed to open URL: ${testUrl}`),
expect.any(Error)
)
);
warnSpy.mockRestore();
});
it('should not try to open URL when actionUrl is not provided', () => {
render();
const button = screen.getByText('No Link');
fireEvent.press(button);
expect(Linking.openURL).not.toHaveBeenCalled();
});
it('should not open URL if canOpenURL returns false', () => {
(Linking.canOpenURL as unknown as jest.Mock).mockResolvedValueOnce(false as any);
const testUrl = 'https://example.com';
render();
const button = screen.getByText('Link');
fireEvent.press(button);
expect(Linking.openURL).not.toHaveBeenCalled();
});
});
describe('Theme support', () => {
it('should apply theme button text color when theme is provided', () => {
const customThemes = {
light: {
colors: {
buttonTextColor: '#FF5733'
}
},
dark: {
colors: {
buttonTextColor: '#FF5733'
}
}
};
renderWithTheme(, customThemes);
const text = screen.getByText('Themed');
expect(text.props.style).toEqual(
expect.arrayContaining([
expect.objectContaining({
color: '#FF5733'
})
])
);
});
it('should handle theme with undefined buttonTextColor', () => {
const customThemes = {
light: {
colors: {}
},
dark: {
colors: {}
}
};
renderWithTheme(, customThemes);
const text = screen.getByText('Default');
expect(text).toBeTruthy();
});
it('should render without theme provider', () => {
expect(() => {
render();
}).not.toThrow();
});
});
describe('Custom styles', () => {
it('should accept and apply custom textStyle', () => {
const customTextStyle = { fontSize: 20, fontWeight: 'bold' as const };
render();
const text = screen.getByText('Styled');
expect(text.props.style).toEqual(
expect.arrayContaining([
expect.objectContaining({
fontSize: 20,
fontWeight: 'bold'
})
])
);
});
it('should accept and apply array of textStyles', () => {
const textStyles = [
{ fontSize: 16 },
{ fontWeight: 'bold' as const },
{ color: 'blue' }
];
render();
const text = screen.getByText('Multi Style');
const styles = JSON.stringify(text.props.style);
expect(styles).toContain('"fontSize":16');
expect(styles).toContain('"fontWeight":"bold"');
expect(styles).toContain('"color":"blue"');
});
it('should accept and apply custom Pressable style', () => {
const customStyle = { padding: 10, backgroundColor: 'red' };
const { getByTestId } = render(
);
const button = getByTestId('button');
expect(button.props.style).toEqual(customStyle);
});
it('should merge theme color with custom textStyle', () => {
const customThemes = {
light: {
colors: {
buttonTextColor: '#FF5733'
}
},
dark: {
colors: {
buttonTextColor: '#FF5733'
}
}
};
const customTextStyle = { fontSize: 18 };
renderWithTheme(
,
customThemes
);
const text = screen.getByText('Merged');
expect(text.props.style).toEqual(
expect.arrayContaining([
expect.objectContaining({
color: '#FF5733'
}),
expect.objectContaining({
fontSize: 18
})
])
);
});
});
describe('Additional Pressable props', () => {
it('should accept disabled prop', () => {
const { getByTestId } = render(
);
const button = getByTestId('button');
expect(button.props.accessibilityState.disabled).toBe(true);
});
it('should accept accessibility props', () => {
const { getByTestId } = render(
);
const button = getByTestId('button');
expect(button.props.accessibilityLabel).toBe('Submit form');
expect(button.props.accessibilityHint).toBe(
'Double tap to submit the form'
);
});
it('should accept testID prop', () => {
const { getByTestId } = render(
);
const button = getByTestId('my-button');
expect(button).toBeTruthy();
});
it('should spread additional Pressable props', () => {
const { getByTestId } = render(
);
const button = getByTestId('button');
expect(button.props.hitSlop).toBe(10);
expect(button.props.accessibilityRole).toBe('button');
});
});
describe('ID prop', () => {
it('should accept id prop without error', () => {
expect(() => {
render();
}).not.toThrow();
});
it('should render correctly with id prop', () => {
render();
const button = screen.getByText('ID Button');
expect(button).toBeTruthy();
});
});
describe('Edge cases', () => {
it('should handle empty title string', () => {
render();
// Should not throw, but may not be visible
expect(() => screen.getByText('')).not.toThrow();
});
it('should handle very long title text', () => {
const longTitle = 'A'.repeat(1000);
render();
const button = screen.getByText(longTitle);
expect(button).toBeTruthy();
});
it('should handle special characters in title', () => {
const specialTitle = '!@#$%^&*()_+-={}[]|:;<>?,./~`';
render();
const button = screen.getByText(specialTitle);
expect(button).toBeTruthy();
});
it('should handle undefined interactId', () => {
render();
const button = screen.getByText('Test');
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalled();
expect((mockOnPress.mock.calls[0] ?? [])[0]).toBeUndefined();
});
it('should handle empty string as actionUrl', () => {
render();
const button = screen.getByText('Empty URL');
fireEvent.press(button);
// Empty string is falsy, so openURL should not be called
expect(Linking.openURL).not.toHaveBeenCalled();
});
});
describe('Callback stability', () => {
it('should maintain stable callback reference', async () => {
const { rerender } = render(
);
const button = screen.getByText('Stable');
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalledTimes(1);
await waitFor(() => expect(Linking.openURL).toHaveBeenCalledTimes(1));
// Rerender with same props
rerender(
);
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalledTimes(2);
await waitFor(() => expect(Linking.openURL).toHaveBeenCalledTimes(2));
});
it('should update callback when dependencies change', () => {
const { rerender } = render(
);
const button = screen.getByText('Dynamic');
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalled();
expect((mockOnPress.mock.calls[0] ?? [])[0]).toBe('id-1');
// Rerender with different interactId
rerender(
);
fireEvent.press(button);
expect((mockOnPress.mock.calls[1] ?? [])[0]).toBe('id-2');
});
});
});