fission-core 0.3.0

Core runtime, state, actions, effects, resources, input, and UI model for Fission
Documentation
use fission_core::{
    ActionRegistry, AppState, CapabilityInvocationPayload, DeepLink, DeepLinkConfig,
    DeepLinkReceived, Effect, Effects, NotificationId, NotificationRequest, NotificationResponse,
    NotificationResponseReceived, SHOW_NOTIFICATION,
};
use fission_core::{BarcodeFormat, BarcodeScanRequest, SCAN_BARCODE};
use fission_core::{BiometricAuthenticateRequest, AUTHENTICATE_BIOMETRIC};
use fission_core::{BluetoothScanRequest, SCAN_BLUETOOTH_DEVICES};
use fission_core::{CameraCaptureRequest, CameraFacing, CAPTURE_PHOTO};
use fission_core::{ClipboardWriteTextRequest, WRITE_CLIPBOARD_TEXT};
use fission_core::{GeolocationPositionRequest, GET_CURRENT_POSITION};
use fission_core::{HapticImpactRequest, HapticImpactStyle, HAPTIC_IMPACT};
use fission_core::{MicrophoneCaptureRequest, CAPTURE_MICROPHONE_AUDIO};
use fission_core::{NfcRecord, NfcScanRequest, NfcTechnology, SCAN_NFC_TAG};
use fission_core::{VolumeSetRequest, VolumeStream, SET_VOLUME_LEVEL};
use fission_core::{WifiScanRequest, SCAN_WIFI_NETWORKS};

#[derive(Debug, Default)]
struct TestState;
impl AppState for TestState {}

#[test]
fn notification_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(42, &mut registry);

    effects.notifications().show(NotificationRequest {
        id: NotificationId::new("n1"),
        title: "Title".into(),
        body: "Body".into(),
        ..Default::default()
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 42);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected notification capability effect");
    };
    assert_eq!(op.capability_name, SHOW_NOTIFICATION.name);
    let decoded: NotificationRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.id, NotificationId::new("n1"));
}

#[test]
fn deep_link_config_and_inbound_actions_are_public_api() {
    let config = DeepLinkConfig::new()
        .scheme("fission")
        .domain("example.com");
    assert!(config.matches("fission://open/tasks/1"));
    assert!(config.matches("https://example.com/tasks/1"));

    let link_action = DeepLinkReceived {
        link: DeepLink::new("fission://open/tasks/1").cold_start(true),
    };
    let _: fission_core::ActionEnvelope = link_action.into();

    let response_action = NotificationResponseReceived {
        response: NotificationResponse {
            notification_id: NotificationId::new("n1"),
            action_id: Some("open".into()),
            deep_link: Some("fission://open/tasks/1".into()),
            user_text: None,
        },
    };
    let _: fission_core::ActionEnvelope = response_action.into();
}

#[test]
fn nfc_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(7, &mut registry);

    effects.nfc().scan_tag(NfcScanRequest {
        technologies: vec![NfcTechnology::Ndef],
        message: Some("Tap a tag".into()),
        timeout_ms: Some(5_000),
        read_multiple_records: true,
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 7);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected NFC capability effect");
    };
    assert_eq!(op.capability_name, SCAN_NFC_TAG.name);
    let decoded: NfcScanRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.technologies, vec![NfcTechnology::Ndef]);
}

#[test]
fn nfc_records_are_public_api() {
    let record = NfcRecord::uri("fission://open/1");
    assert_eq!(record.type_name, b"U".to_vec());
}

#[test]
fn biometric_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(11, &mut registry);

    effects
        .biometrics()
        .authenticate(BiometricAuthenticateRequest {
            reason: "Unlock secure data".into(),
            ..Default::default()
        });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 11);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected biometric capability effect");
    };
    assert_eq!(op.capability_name, AUTHENTICATE_BIOMETRIC.name);
    let decoded: BiometricAuthenticateRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.reason, "Unlock secure data");
}

#[test]
fn bluetooth_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(12, &mut registry);

    effects.bluetooth().scan_devices(BluetoothScanRequest {
        service_uuids: vec!["180D".into()],
        ..Default::default()
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 12);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected Bluetooth capability effect");
    };
    assert_eq!(op.capability_name, SCAN_BLUETOOTH_DEVICES.name);
    let decoded: BluetoothScanRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.service_uuids, vec!["180D"]);
}

#[test]
fn barcode_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(13, &mut registry);

    effects.barcode_scanner().scan(BarcodeScanRequest {
        formats: vec![BarcodeFormat::QrCode],
        prompt: Some("Scan code".into()),
        ..Default::default()
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 13);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected barcode scanner capability effect");
    };
    assert_eq!(op.capability_name, SCAN_BARCODE.name);
    let decoded: BarcodeScanRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.formats, vec![BarcodeFormat::QrCode]);
}

#[test]
fn camera_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(15, &mut registry);

    effects.camera().capture_photo(CameraCaptureRequest {
        facing: CameraFacing::Back,
        ..Default::default()
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 15);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected camera capability effect");
    };
    assert_eq!(op.capability_name, CAPTURE_PHOTO.name);
    let decoded: CameraCaptureRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.facing, CameraFacing::Back);
}

#[test]
fn clipboard_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(17, &mut registry);

    effects.clipboard().write_text(ClipboardWriteTextRequest {
        text: "copied".into(),
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 17);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected clipboard capability effect");
    };
    assert_eq!(op.capability_name, WRITE_CLIPBOARD_TEXT.name);
    let decoded: ClipboardWriteTextRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.text, "copied");
}

#[test]
fn geolocation_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(19, &mut registry);

    effects
        .geolocation()
        .current_position(GeolocationPositionRequest {
            high_accuracy: true,
            ..Default::default()
        });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 19);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected geolocation capability effect");
    };
    assert_eq!(op.capability_name, GET_CURRENT_POSITION.name);
    let decoded: GeolocationPositionRequest = serde_json::from_slice(&op.request).unwrap();
    assert!(decoded.high_accuracy);
}

#[test]
fn haptic_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(23, &mut registry);

    effects.haptics().impact(HapticImpactRequest {
        style: HapticImpactStyle::Heavy,
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 23);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected haptic capability effect");
    };
    assert_eq!(op.capability_name, HAPTIC_IMPACT.name);
    let decoded: HapticImpactRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.style, HapticImpactStyle::Heavy);
}

#[test]
fn microphone_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(29, &mut registry);

    effects
        .microphone()
        .capture_audio(MicrophoneCaptureRequest {
            duration_ms: 2_500,
            ..Default::default()
        });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 29);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected microphone capability effect");
    };
    assert_eq!(op.capability_name, CAPTURE_MICROPHONE_AUDIO.name);
    let decoded: MicrophoneCaptureRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.duration_ms, 2_500);
}

#[test]
fn wifi_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(31, &mut registry);

    effects.wifi().scan_networks(WifiScanRequest {
        ssid_prefix: Some("Fis".into()),
        ..Default::default()
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 31);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected Wi-Fi capability effect");
    };
    assert_eq!(op.capability_name, SCAN_WIFI_NETWORKS.name);
    let decoded: WifiScanRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.ssid_prefix.as_deref(), Some("Fis"));
}

#[test]
fn volume_convenience_builder_emits_capability_effect() {
    let mut registry = ActionRegistry::<TestState>::new();
    let mut effects = Effects::new(37, &mut registry);

    effects.volume().set_level(VolumeSetRequest {
        stream: VolumeStream::Media,
        level: 75,
        muted: Some(false),
    });

    assert_eq!(effects.out.len(), 1);
    assert_eq!(effects.out[0].req_id, 37);
    let Effect::Capability(CapabilityInvocationPayload::Operation(op)) = &effects.out[0].effect
    else {
        panic!("expected volume capability effect");
    };
    assert_eq!(op.capability_name, SET_VOLUME_LEVEL.name);
    let decoded: VolumeSetRequest = serde_json::from_slice(&op.request).unwrap();
    assert_eq!(decoded.level, 75);
}