use dioxus_audio::recorder::{
CompletionDisposition, RecorderConstraintCapabilities, RecorderLifecycle, RecorderOptions,
RecorderStatus, RecordingChunkDelivery, RecordingCompletionCause, RecordingConstraint,
RecordingConstraints, RecordingOutcome, RecordingSourceSettings,
};
use dioxus_audio::{AudioError, AudioErrorKind, RecordingChunk};
#[test]
fn stopped_recording_completes_once_and_returns_to_idle() {
let mut recorder = RecorderLifecycle::default();
let recording_id = recorder.start().unwrap();
assert_eq!(recorder.status(), &RecorderStatus::Preparing);
assert!(recorder.started(recording_id));
assert_eq!(recorder.status(), &RecorderStatus::Recording);
recorder.stop().unwrap();
assert_eq!(recorder.status(), &RecorderStatus::Stopping);
assert_eq!(
recorder.begin_finalize(recording_id),
Some(CompletionDisposition::Save)
);
assert_eq!(
recorder.completion_cause(recording_id),
Some(RecordingCompletionCause::Requested)
);
assert!(recorder.start().is_err());
assert_eq!(recorder.status(), &RecorderStatus::Stopping);
assert!(recorder.complete_finalize(recording_id));
assert_eq!(recorder.status(), &RecorderStatus::Idle);
assert_eq!(recorder.begin_finalize(recording_id), None);
assert!(recorder.start().is_err());
recorder.clear_completed();
assert!(recorder.start().is_ok());
}
#[test]
fn cancelled_recording_is_discarded() {
let mut recorder = RecorderLifecycle::default();
let recording_id = recorder.start().unwrap();
recorder.started(recording_id);
recorder.cancel().unwrap();
assert_eq!(
recorder.begin_finalize(recording_id),
Some(CompletionDisposition::Discard)
);
assert!(recorder.complete_finalize(recording_id));
}
#[test]
fn browser_initiated_stop_finalizes_the_partial_recording() {
let mut recorder = RecorderLifecycle::default();
let recording_id = recorder.start().unwrap();
recorder.started(recording_id);
assert_eq!(
recorder.begin_finalize(recording_id),
Some(CompletionDisposition::Save)
);
assert_eq!(
recorder.completion_cause(recording_id),
Some(RecordingCompletionCause::UnexpectedEnd)
);
assert_eq!(recorder.status(), &RecorderStatus::Stopping);
assert!(recorder.stop().is_err());
assert!(recorder.cancel().is_err());
assert!(recorder.complete_finalize(recording_id));
assert_eq!(recorder.status(), &RecorderStatus::Idle);
}
#[test]
fn source_end_completes_an_active_or_paused_recording_with_its_cause() {
for paused in [false, true] {
let mut recorder = RecorderLifecycle::default();
let recording_id = recorder.start().unwrap();
recorder.started(recording_id);
if paused {
recorder.pause().unwrap();
}
assert!(recorder.source_ended());
assert_eq!(recorder.status(), &RecorderStatus::Stopping);
assert_eq!(
recorder.completion_cause(recording_id),
Some(RecordingCompletionCause::SourceEnded)
);
assert_eq!(
recorder.begin_finalize(recording_id),
Some(CompletionDisposition::Save)
);
}
}
#[test]
fn an_earlier_completion_or_discard_wins_over_source_end() {
let mut completed = RecorderLifecycle::default();
let completed_id = completed.start().unwrap();
completed.started(completed_id);
completed.stop().unwrap();
assert!(!completed.source_ended());
assert_eq!(
completed.completion_cause(completed_id),
Some(RecordingCompletionCause::Requested)
);
assert_eq!(
completed.begin_finalize(completed_id),
Some(CompletionDisposition::Save)
);
let mut discarded = RecorderLifecycle::default();
let discarded_id = discarded.start().unwrap();
discarded.started(discarded_id);
discarded.cancel().unwrap();
assert!(!discarded.source_ended());
assert_eq!(discarded.completion_cause(discarded_id), None);
assert_eq!(
discarded.begin_finalize(discarded_id),
Some(CompletionDisposition::Discard)
);
}
#[test]
fn pause_and_resume_are_valid_only_during_recording() {
let mut recorder = RecorderLifecycle::default();
assert!(recorder.pause().is_err());
let recording_id = recorder.start().unwrap();
recorder.started(recording_id);
recorder.pause().unwrap();
assert_eq!(recorder.status(), &RecorderStatus::Paused);
recorder.resume().unwrap();
assert_eq!(recorder.status(), &RecorderStatus::Recording);
}
#[test]
fn stale_backend_events_cannot_replace_a_new_recording() {
let mut recorder = RecorderLifecycle::default();
let stale = recorder.start().unwrap();
recorder.cancel().unwrap();
let current = recorder.start().unwrap();
assert!(!recorder.started(stale));
assert_eq!(recorder.status(), &RecorderStatus::Preparing);
assert!(recorder.started(current));
}
#[test]
fn recording_chunks_keep_identity_and_sequence_across_pause_resume_and_stop() {
let mut recorder = RecorderLifecycle::default();
let recording_id = recorder.start().unwrap();
recorder.started(recording_id);
let first = RecordingChunk {
recording_id,
sequence: recorder.next_chunk_sequence(recording_id).unwrap(),
bytes: vec![1, 2, 3],
media_type: "audio/webm;codecs=opus".to_string(),
};
recorder.pause().unwrap();
recorder.resume().unwrap();
let second = RecordingChunk {
recording_id,
sequence: recorder.next_chunk_sequence(recording_id).unwrap(),
bytes: vec![4, 5],
media_type: "audio/webm;codecs=opus".to_string(),
};
recorder.stop().unwrap();
let final_chunk = RecordingChunk {
recording_id,
sequence: recorder.next_chunk_sequence(recording_id).unwrap(),
bytes: vec![6],
media_type: "audio/webm;codecs=opus".to_string(),
};
assert_eq!(first.sequence, 0);
assert_eq!(second.sequence, 1);
assert_eq!(final_chunk.sequence, 2);
assert_eq!(first.recording_id, second.recording_id);
assert_eq!(second.recording_id, final_chunk.recording_id);
assert_eq!(first.bytes, vec![1, 2, 3]);
assert_eq!(first.media_type, "audio/webm;codecs=opus");
assert_eq!(
recorder.begin_finalize(recording_id),
Some(CompletionDisposition::Save)
);
assert!(recorder.complete_finalize(recording_id));
assert_eq!(recorder.next_chunk_sequence(recording_id), None);
}
#[test]
fn best_effort_chunk_boundaries_preserve_identity_and_sequence_while_active_or_paused() {
let mut recorder = RecorderLifecycle::default();
assert!(recorder.request_chunk_boundary().is_err());
let recording_id = recorder.start().unwrap();
recorder.started(recording_id);
recorder.request_chunk_boundary().unwrap();
assert_eq!(recorder.next_chunk_sequence(recording_id), Some(0));
recorder.pause().unwrap();
recorder.request_chunk_boundary().unwrap();
assert_eq!(recorder.next_chunk_sequence(recording_id), Some(1));
recorder.resume().unwrap();
recorder.stop().unwrap();
assert!(recorder.request_chunk_boundary().is_err());
}
#[test]
fn discard_suppresses_chunks_and_a_restart_gets_new_identity_and_sequence() {
let mut recorder = RecorderLifecycle::default();
let discarded_id = recorder.start().unwrap();
recorder.started(discarded_id);
assert_eq!(recorder.next_chunk_sequence(discarded_id), Some(0));
recorder.cancel().unwrap();
assert_eq!(recorder.next_chunk_sequence(discarded_id), None);
assert_eq!(
recorder.begin_finalize(discarded_id),
Some(CompletionDisposition::Discard)
);
assert!(recorder.complete_finalize(discarded_id));
let restarted_id = recorder.start().unwrap();
assert_ne!(restarted_id, discarded_id);
recorder.started(restarted_id);
assert_eq!(recorder.next_chunk_sequence(restarted_id), Some(0));
assert_eq!(recorder.next_chunk_sequence(discarded_id), None);
}
#[test]
fn recording_outcomes_expose_the_recording_identity() {
let mut recorder = RecorderLifecycle::default();
let recording_id = recorder.start().unwrap();
let error = AudioError::new(AudioErrorKind::RecorderFailure, "encoder stopped");
let completed = RecordingOutcome::Completed {
recording_id,
cause: RecordingCompletionCause::Requested,
};
let source_ended = RecordingOutcome::Completed {
recording_id,
cause: RecordingCompletionCause::SourceEnded,
};
let discarded = RecordingOutcome::Discarded(recording_id);
let failed = RecordingOutcome::Failed {
recording_id,
error: error.clone(),
};
assert_eq!(completed.recording_id(), recording_id);
assert_eq!(source_ended.recording_id(), recording_id);
assert_ne!(completed, source_ended);
assert_eq!(discarded.recording_id(), recording_id);
assert_eq!(failed.recording_id(), recording_id);
assert_eq!(
failed,
RecordingOutcome::Failed {
recording_id,
error,
}
);
}
#[test]
fn backend_failures_are_observable_and_restartable() {
let mut recorder = RecorderLifecycle::default();
let recording_id = recorder.start().unwrap();
let error = AudioError::new(AudioErrorKind::PermissionDenied, "microphone denied");
assert!(recorder.failed(recording_id, error.clone()));
assert_eq!(recorder.status(), &RecorderStatus::Failed(error));
assert!(recorder.start().is_ok());
}
#[test]
fn recorder_options_reject_invalid_analysis_configuration() {
let mut options = RecorderOptions::default();
options.fft_size = 100;
assert!(options.validate().is_err());
let mut options = RecorderOptions::default();
options.smoothing = 1.5;
assert!(options.validate().is_err());
let mut options = RecorderOptions::default();
options.peak_interval = std::time::Duration::ZERO;
assert!(options.validate().is_err());
let mut options = RecorderOptions::default();
options.chunk_delivery = Some(RecordingChunkDelivery::new(
std::time::Duration::ZERO,
|_| {},
));
assert!(options.validate().is_err());
let mut recorder = RecorderLifecycle::default();
let error = options.validate().unwrap_err();
assert!(recorder.configuration_failed(error.clone()));
assert_eq!(recorder.status(), &RecorderStatus::Failed(error));
}
#[test]
fn recorder_constraints_express_the_portable_startup_subset() {
let constraints = RecordingConstraints {
channel_count: Some(RecordingConstraint::Exact(1)),
sample_rate: Some(RecordingConstraint::Ideal(48_000)),
echo_cancellation: Some(RecordingConstraint::Ideal(false)),
noise_suppression: Some(RecordingConstraint::Exact(false)),
latency: Some(RecordingConstraint::Ideal(
std::time::Duration::from_millis(20),
)),
};
let snapshot = constraints.clone();
let mut changed = constraints;
changed.sample_rate = Some(RecordingConstraint::Exact(44_100));
assert_eq!(
snapshot.sample_rate,
Some(RecordingConstraint::Ideal(48_000))
);
assert_eq!(
snapshot.latency,
Some(RecordingConstraint::Ideal(
std::time::Duration::from_millis(20)
))
);
assert_ne!(snapshot, changed);
}
#[test]
fn recorder_capabilities_and_effective_settings_are_distinct_values() {
let capabilities = RecorderConstraintCapabilities {
channel_count: true,
sample_rate: true,
echo_cancellation: true,
noise_suppression: false,
latency: true,
};
let settings = RecordingSourceSettings {
channel_count: Some(1),
sample_rate: Some(48_000),
echo_cancellation: Some(false),
noise_suppression: None,
latency: Some(std::time::Duration::from_millis(10)),
};
assert!(!capabilities.noise_suppression);
assert_eq!(settings.noise_suppression, None);
assert_eq!(settings.sample_rate, Some(48_000));
}
#[test]
fn exact_constraint_failures_preserve_the_rejected_constraint() {
let error = AudioError::overconstrained("sampleRate", "sample rate is unavailable");
assert_eq!(error.kind(), AudioErrorKind::Overconstrained);
assert_eq!(error.overconstrained_constraint(), Some("sampleRate"));
assert_eq!(error.message(), "sample rate is unavailable");
}
#[test]
fn overconstraint_failure_does_not_invent_missing_detail() {
let error = AudioError::overconstrained("", "constraints are unavailable");
assert_eq!(error.kind(), AudioErrorKind::Overconstrained);
assert_eq!(error.overconstrained_constraint(), None);
}