[feat](client): show what stands between this phone and a real alarm
The blocking grants (exact alarms, full-screen intent, notifications) and the advisory battery exemption are surfaced as readiness, not as warnings that still allow arming. Listening state is read from the persisted native record on resume so a process-killed service cannot leave a stale armed claim on screen.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import type { AlarmPermissions } from '../src/services/alarmNative';
|
||||
import {
|
||||
READINESS_COPY,
|
||||
canDeliverRealAlarm,
|
||||
deliverySummary,
|
||||
takeoverReadiness,
|
||||
} from '../src/lib/takeoverReadiness';
|
||||
|
||||
const ALL_GRANTED: AlarmPermissions = {
|
||||
notifications: true,
|
||||
fullScreenIntent: true,
|
||||
exactAlarm: true,
|
||||
batteryUnrestricted: true,
|
||||
canForceVolume: true,
|
||||
};
|
||||
|
||||
function perms(patch: Partial<AlarmPermissions>): AlarmPermissions {
|
||||
return { ...ALL_GRANTED, ...patch };
|
||||
}
|
||||
|
||||
describe('takeover readiness', () => {
|
||||
it('is unknown (and renders nothing) while permissions are still loading', () => {
|
||||
const readiness = takeoverReadiness(null);
|
||||
expect(readiness.known).toBe(false);
|
||||
expect(readiness.blocked).toBe(false);
|
||||
expect(readiness.blockers).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('is complete when every grant is present', () => {
|
||||
const readiness = takeoverReadiness(ALL_GRANTED);
|
||||
expect(readiness.known).toBe(true);
|
||||
expect(readiness.blocked).toBe(false);
|
||||
expect(readiness.blockers).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('treats missing exact alarms as blocking, because the alarm clock is the delivery', () => {
|
||||
const readiness = takeoverReadiness(perms({ exactAlarm: false }));
|
||||
expect(readiness.blocked).toBe(true);
|
||||
const exact = readiness.blockers.find(b => b.key === 'exactAlarm');
|
||||
expect(exact).toBeDefined();
|
||||
expect(exact?.blocking).toBe(true);
|
||||
expect(exact?.body).toContain('cannot fire a real alarm');
|
||||
});
|
||||
|
||||
it('treats a missing full-screen intent as blocking, never as a notification fallback', () => {
|
||||
const readiness = takeoverReadiness(perms({ fullScreenIntent: false }));
|
||||
expect(readiness.blocked).toBe(true);
|
||||
const fsi = readiness.blockers.find(b => b.key === 'fullScreenIntent');
|
||||
expect(fsi).toBeDefined();
|
||||
expect(fsi?.blocking).toBe(true);
|
||||
expect(fsi?.body).toContain('cannot take over the screen');
|
||||
// The old design promised a notification instead. That outcome is gone.
|
||||
expect(fsi?.body).not.toMatch(/instead of taking over/i);
|
||||
expect(fsi?.body).toContain('not usable for alerts');
|
||||
expect(fsi?.actionLabel).toBe('Open settings');
|
||||
});
|
||||
|
||||
it('treats missing notifications as blocking (the foreground service needs one)', () => {
|
||||
const readiness = takeoverReadiness(perms({ notifications: false }));
|
||||
expect(readiness.blocked).toBe(true);
|
||||
const notifications = readiness.blockers.find(b => b.key === 'notifications');
|
||||
expect(notifications?.blocking).toBe(true);
|
||||
expect(notifications?.body).toContain('cannot keep listening while the app is closed');
|
||||
});
|
||||
|
||||
it('keeps the battery exemption advisory, not blocking', () => {
|
||||
const readiness = takeoverReadiness(perms({ batteryUnrestricted: false }));
|
||||
expect(readiness.known).toBe(true);
|
||||
expect(readiness.blocked).toBe(false);
|
||||
const battery = readiness.blockers.find(b => b.key === 'battery');
|
||||
expect(battery?.blocking).toBe(false);
|
||||
expect(battery?.body).toBe(READINESS_COPY.battery.body);
|
||||
});
|
||||
|
||||
it('reports every blocker in priority order when nothing is granted', () => {
|
||||
const readiness = takeoverReadiness(
|
||||
perms({
|
||||
exactAlarm: false,
|
||||
notifications: false,
|
||||
fullScreenIntent: false,
|
||||
batteryUnrestricted: false,
|
||||
}),
|
||||
);
|
||||
expect(readiness.blocked).toBe(true);
|
||||
expect(readiness.blockers.map(b => b.key)).toEqual([
|
||||
'exactAlarm',
|
||||
'fullScreenIntent',
|
||||
'notifications',
|
||||
'battery',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canDeliverRealAlarm', () => {
|
||||
it('is true only when every blocking grant is present', () => {
|
||||
expect(canDeliverRealAlarm(ALL_GRANTED)).toBe(true);
|
||||
expect(canDeliverRealAlarm(perms({ exactAlarm: false }))).toBe(false);
|
||||
expect(canDeliverRealAlarm(perms({ fullScreenIntent: false }))).toBe(false);
|
||||
expect(canDeliverRealAlarm(perms({ notifications: false }))).toBe(false);
|
||||
// Advice, not a gate.
|
||||
expect(canDeliverRealAlarm(perms({ batteryUnrestricted: false }))).toBe(true);
|
||||
expect(canDeliverRealAlarm(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delivery summary', () => {
|
||||
it('never claims a real alarm while the takeover is blocked', () => {
|
||||
const summary = deliverySummary(perms({ fullScreenIntent: false }));
|
||||
expect(summary).toBe(
|
||||
'This device cannot take over the screen — the full-screen takeover is not allowed.',
|
||||
);
|
||||
expect(summary).not.toMatch(/maximum volume/);
|
||||
expect(summary).not.toMatch(/notification/i);
|
||||
});
|
||||
|
||||
it('names the exact-alarm gap first, since that is the delivery mechanism', () => {
|
||||
expect(deliverySummary(perms({ exactAlarm: false }))).toBe(
|
||||
'This device cannot fire a real alarm — exact alarms are not allowed.',
|
||||
);
|
||||
});
|
||||
|
||||
it('says it cannot keep listening without notifications', () => {
|
||||
expect(deliverySummary(perms({ notifications: false }))).toBe(
|
||||
'This device cannot keep listening while the app is closed — notifications are not allowed.',
|
||||
);
|
||||
});
|
||||
|
||||
it('claims a real alarm only when every blocking grant is present', () => {
|
||||
expect(deliverySummary(ALL_GRANTED)).toBe(
|
||||
'This device fires a real alarm at maximum volume when the owner triggers the alert.',
|
||||
);
|
||||
// Battery is advisory: it does not change the promise.
|
||||
expect(deliverySummary(perms({ batteryUnrestricted: false }))).toBe(
|
||||
'This device fires a real alarm at maximum volume when the owner triggers the alert.',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not claim readiness while permissions are unread', () => {
|
||||
expect(deliverySummary(null)).toMatch(/Checking/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user