import React from 'react';
import renderer, { act, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer';
import { TakeoverReadiness } from '../src/components/TakeoverReadiness';
import type { AlarmPermissions } from '../src/services/alarmNative';
// The banner's logic and copy are what matter here; the house visual primitives
// need their native-ish modules stubbed so the component can render under Jest.
jest.mock('heroui-native/text', () => ({ Text: require('react-native').Text }));
jest.mock('heroui-native/spinner', () => ({ Spinner: require('react-native').View }));
jest.mock('heroui-native/surface', () => ({ Surface: require('react-native').View }));
jest.mock('lucide-react-native', () => {
const ReactMock = require('react');
const { View: RNView } = require('react-native');
const Icon = () => ReactMock.createElement(RNView);
return {
BellOff: Icon,
CircleAlert: Icon,
CircleDashed: Icon,
ShieldAlert: Icon,
ShieldCheck: Icon,
Siren: Icon,
TriangleAlert: Icon,
WifiOff: Icon,
RefreshCw: Icon,
};
});
const ALL_GRANTED: AlarmPermissions = {
notifications: true,
fullScreenIntent: true,
exactAlarm: true,
batteryUnrestricted: true,
canForceVolume: true,
};
function render(node: React.ReactElement): ReactTestRenderer {
let tree: ReactTestRenderer | undefined;
act(() => {
tree = renderer.create(node);
});
return tree as ReactTestRenderer;
}
/** Concatenates every rendered string so assertions can match the visible copy. */
function flatText(node: ReactTestInstance | string): string {
if (typeof node === 'string') return node;
return node.children.map(child => flatText(child)).join(' ');
}
describe('TakeoverReadiness banner', () => {
it('renders the explanation and a settings action when the takeover is not granted', () => {
const onFix = jest.fn();
const tree = render(
,
);
const text = flatText(tree.root);
expect(text).toContain('This device cannot deliver a real alarm yet');
expect(text).toContain(
'Without this, this device cannot take over the screen when the alert fires.',
);
expect(text).toContain('Open settings');
// Blocking, not advisory: it must say the device is not armed meanwhile.
expect(text).toContain('this device is not armed and must not be relied on for alerts');
const action = tree.root.findAll(
node => node.props?.accessibilityLabel === 'Open settings — Full-screen takeover',
)[0];
expect(action).toBeDefined();
act(() => {
action.props.onPress();
});
expect(onFix).toHaveBeenCalledWith('fullScreenIntent');
});
it('renders nothing once the takeover path is complete', () => {
const tree = render(
,
);
expect(tree.toJSON()).toBeNull();
});
it('renders nothing while permissions are still being read', () => {
const tree = render();
expect(tree.toJSON()).toBeNull();
});
it('renders nothing off-Android, where the grants do not apply', () => {
const tree = render(
,
);
expect(tree.toJSON()).toBeNull();
});
it('keeps the battery exemption advisory but still offers the fix', () => {
const tree = render(
,
);
const text = flatText(tree.root);
expect(text).toContain('This device may miss a trigger in the background');
expect(text).toContain('Background reliability');
expect(text).not.toContain('cannot deliver a real alarm yet');
});
});