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
10 changes: 9 additions & 1 deletion packages/messaging/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
module.exports = {
preset: "react-native",
testMatch: ["<rootDir>/src/ui/**/*.spec.tsx"],
};
collectCoverage: true,
collectCoverageFrom: [
"<rootDir>/src/ui/components/**/*.{ts,tsx}", // only components
"!**/*.spec.tsx",
"!**/__tests__/**",
],
coverageReporters: ["text", "text-summary", "html", "lcov"],
coverageDirectory: "<rootDir>/coverage",
};
23 changes: 21 additions & 2 deletions packages/messaging/src/ui/components/Button/Button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
*/
import { fireEvent, render, screen } from '@testing-library/react-native';
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import { Linking } from 'react-native';
import Button from './Button';
import { ThemeProvider } from '../../theme/ThemeProvider';
import Button from './Button';

// Mock Linking.openURL
jest.spyOn(Linking, 'openURL');
Expand Down Expand Up @@ -157,6 +157,25 @@ describe('Button', () => {
expect(Linking.openURL).toHaveBeenCalledWith(testUrl);
});

it('should warn if openURL throws', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const testUrl = 'https://example.com';
(Linking.openURL as unknown as jest.Mock).mockImplementationOnce(() => {
throw new Error('open failed');
});

render(<Button title="Link" actionUrl={testUrl} />);
const button = screen.getByText('Link');

expect(() => fireEvent.press(button)).not.toThrow();
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(<Button title="No Link" onPress={mockOnPress} />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
language governing permissions and limitations under the License.
*/

import React from 'react';
import { render, screen } from '@testing-library/react-native';
import React from 'react';
import { Dimensions, Text } from 'react-native';
import EmptyState from './EmptyState';
import { ContentCardContainer } from './ContentCardContainer';

// Mock hooks used by the container
Expand Down Expand Up @@ -68,44 +69,143 @@ describe('ContentCardContainer', () => {
jest.spyOn(Dimensions, 'get').mockReturnValue({ width: 400, height: 800, scale: 2, fontScale: 2 } as any);
});

it('renders loading state', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: true, error: null });

const Loading = <Text>Loading...</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} LoadingComponent={Loading} />);
expect(screen.getByText('Loading...')).toBeTruthy();
describe('outer container states', () => {
it('renders loading state', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: true, error: null });

const Loading = <Text>Loading...</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} LoadingComponent={Loading} />);
expect(screen.getByText('Loading...')).toBeTruthy();
});

it('renders error state', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: new Error('x') });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: false, error: null });

const ErrorComp = <Text>Error!</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} ErrorComponent={ErrorComp} />);
expect(screen.getByText('Error!')).toBeTruthy();
});

it('renders fallback when no content yet', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: undefined, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: false, error: null });

const Fallback = <Text>Fallback</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} FallbackComponent={Fallback} />);
expect(screen.getByText('Fallback')).toBeTruthy();
});

it('renders outer LoadingComponent when container is loading', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: undefined, isLoading: true, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: false, error: null });

const Loading = <Text testID="outer-loading">Loading outer...</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} LoadingComponent={Loading} />);
expect(screen.getByTestId('outer-loading')).toBeTruthy();
});
});

it('renders error state', () => {
// Outer container handles ErrorComponent when useContentContainer has an error
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: new Error('x') });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: false, error: null });

const ErrorComp = <Text>Error!</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} ErrorComponent={ErrorComp} />);
expect(screen.getByText('Error!')).toBeTruthy();
describe('empty content rendering', () => {
it('renders empty state when content is empty', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: [], isLoading: false, error: null });

const CC: any = ContentCardContainer;
render(<CC surface={surface} />);
expect(screen.getByText('No Content Available')).toBeTruthy();
});

it('uses light image when colorScheme is null and falls back to default message', () => {
jest.spyOn(require('react-native'), 'useColorScheme').mockReturnValue(null);
const settings = {
...baseSettings,
content: {
...baseSettings.content,
emptyStateSettings: {
image: { light: { url: 'https://example.com/light-only.png' } }
} as any
}
};
(useContentContainer as jest.Mock).mockReturnValue({ settings, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: [], isLoading: false, error: null });

const CC: any = ContentCardContainer;
const { UNSAFE_getByType } = render(<CC surface={surface} />);
const empty = UNSAFE_getByType(EmptyState);
expect(empty.props.image).toBe('https://example.com/light-only.png');
expect(empty.props.text).toBe('No Content Available');
});
});

it('renders fallback when no content yet', () => {
// Outer container handles FallbackComponent when settings are missing
(useContentContainer as jest.Mock).mockReturnValue({ settings: undefined, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: false, error: null });
describe('heading and layout', () => {
it('sets heading color based on color scheme: dark -> #FFFFFF', () => {
jest.spyOn(require('react-native'), 'useColorScheme').mockReturnValue('dark');
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
const template = { id: '1', type: 'SmallImage', data: { content: { title: { content: 'T' }, body: { content: 'B' }, image: { url: 'u' } } } };
(useContentCardUI as jest.Mock).mockReturnValue({ content: [template], isLoading: false, error: null });
const CC: any = ContentCardContainer;
const { getByText } = render(<CC surface={surface} />);
const heading = getByText('Heading');
const styles = Array.isArray(heading.props.style) ? heading.props.style : [heading.props.style];
expect(styles.some((s: any) => s && s.color === '#FFFFFF')).toBe(true);
});
});

const Fallback = <Text>Fallback</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} FallbackComponent={Fallback} />);
expect(screen.getByText('Fallback')).toBeTruthy();
describe('inner container states', () => {
it('renders inner ErrorComponent when data hook errors', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: false, error: new Error('inner') });

const ErrorComp = <Text testID="inner-error">Inner Error!</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} ErrorComponent={ErrorComp} />);
expect(screen.getByTestId('inner-error')).toBeTruthy();
});

it('uses provided EmptyComponent and passes empty state settings (inner)', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: [], isLoading: false, error: null });

const EmptyStub = ({ message }: any) => (
<Text testID="inner-empty">{message?.content}</Text>
);
const CC: any = ContentCardContainer;
render(<CC surface={surface} EmptyComponent={<EmptyStub />} />);
expect(screen.getByTestId('inner-empty')).toBeTruthy();
expect(screen.getByText('No Content Available')).toBeTruthy();
});

it('renders inner FallbackComponent when content is undefined and settings exist', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: undefined, isLoading: false, error: null });

const Fallback = <Text testID="inner-fallback">Inner Fallback</Text> as any;
const CC: any = ContentCardContainer;
render(<CC surface={surface} FallbackComponent={Fallback} />);
expect(screen.getByTestId('inner-fallback')).toBeTruthy();
});
});

it('renders empty state when content is empty', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
(useContentCardUI as jest.Mock).mockReturnValue({ content: [], isLoading: false, error: null });
describe('renderItem passthrough', () => {
it('passes expected props to ContentCardView via renderItem (horizontal)', () => {
(useContentContainer as jest.Mock).mockReturnValue({ settings: baseSettings, isLoading: false, error: null });
const template = { id: '1', type: 'SmallImage', data: { content: { title: { content: 'T' }, body: { content: 'B' }, image: { url: 'u' } } } };
(useContentCardUI as jest.Mock).mockReturnValue({ content: [template], isLoading: false, error: null });

const CC: any = ContentCardContainer;
render(<CC surface={surface} />);

const CC: any = ContentCardContainer;
render(<CC surface={surface} />);
expect(screen.getByText('No Content Available')).toBeTruthy();
expect(mockContentCardView).toHaveBeenCalled();
const args = mockContentCardView.mock.calls[0][0];
expect(args.template).toEqual(template);
expect(args.style).toEqual(expect.arrayContaining([expect.anything()]));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export function ContentCardContainer<T extends ContentTemplate>({
settings={settings}
surface={surface}
LoadingComponent={LoadingComponent}
ErrorComponent={ErrorComponent}
FallbackComponent={FallbackComponent}
{...props}
/>
);
Expand Down
Loading