Skip to main content

tauri_plugin_window_system/
commands.rs

1use crate::events::{emit_registry_changed, RegistryChangeKind};
2use crate::lifecycle::{
3    apply_window_geometry, attach_native_parent, attach_window_event_handlers,
4    close_window_tree as close_window_tree_impl,
5};
6use crate::message::{
7    broadcast_window_message as broadcast_window_message_impl,
8    send_window_message as send_window_message_impl, BroadcastWindowMessageRequest,
9    SendWindowMessageRequest, WindowMessageDispatchResult,
10};
11use crate::registry::{WindowDescriptor, WindowGeometry, WindowRegistry};
12use crate::sync_live_window;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::HashSet;
16use std::time::Instant;
17use tauri::{
18    command, AppHandle, Manager, Runtime, State, WebviewUrl, WebviewWindowBuilder, Window,
19};
20
21#[derive(Debug, Deserialize)]
22pub struct OpenWindowRequest {
23    pub label: String,
24    pub url: Option<String>,
25    pub parent: Option<String>,
26    pub title: Option<String>,
27    pub geometry: Option<WindowGeometry>,
28}
29
30#[derive(Debug, Clone, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct RestoreWindowsResult {
33    pub restored: Vec<WindowDescriptor>,
34    pub already_alive: Vec<String>,
35    pub skipped: Vec<RestoreSkippedWindow>,
36}
37
38#[derive(Debug, Clone, Serialize)]
39pub struct RestoreSkippedWindow {
40    pub label: String,
41    pub parent: Option<String>,
42    pub reason: RestoreSkipReason,
43}
44
45#[derive(Debug, Clone, Copy, Serialize)]
46#[serde(rename_all = "kebab-case")]
47pub enum RestoreSkipReason {
48    MissingParent,
49}
50
51#[derive(Debug)]
52struct NormalizedOpenWindowRequest {
53    label: String,
54    url: String,
55    parent: Option<String>,
56    title: Option<String>,
57    geometry: Option<WindowGeometry>,
58}
59
60#[cfg(any(debug_assertions, feature = "window-timings"))]
61struct OperationTiming {
62    operation: &'static str,
63    subject: Option<String>,
64    started_at: Instant,
65    last_mark: Instant,
66}
67
68#[cfg(not(any(debug_assertions, feature = "window-timings")))]
69struct OperationTiming;
70
71impl OperationTiming {
72    #[cfg(any(debug_assertions, feature = "window-timings"))]
73    fn new(operation: &'static str, subject: Option<&str>) -> Self {
74        let now = Instant::now();
75        Self {
76            operation,
77            subject: subject.map(str::to_string),
78            started_at: now,
79            last_mark: now,
80        }
81    }
82
83    #[cfg(not(any(debug_assertions, feature = "window-timings")))]
84    fn new(_operation: &'static str, _subject: Option<&str>) -> Self {
85        Self
86    }
87
88    #[cfg(any(debug_assertions, feature = "window-timings"))]
89    fn mark(&mut self, stage: &str) {
90        let now = Instant::now();
91        let stage_elapsed = now.duration_since(self.last_mark);
92        let total_elapsed = now.duration_since(self.started_at);
93        log_operation_timing(
94            self.operation,
95            self.subject.as_deref(),
96            stage,
97            stage_elapsed.as_millis(),
98            total_elapsed.as_millis(),
99        );
100        self.last_mark = now;
101    }
102
103    #[cfg(not(any(debug_assertions, feature = "window-timings")))]
104    fn mark(&mut self, _stage: &str) {}
105
106    #[cfg(any(debug_assertions, feature = "window-timings"))]
107    fn finish(self, summary: Option<&str>) {
108        let total_elapsed = Instant::now().duration_since(self.started_at);
109        log_operation_completion(
110            self.operation,
111            self.subject.as_deref(),
112            total_elapsed.as_millis(),
113            summary,
114        );
115    }
116
117    #[cfg(not(any(debug_assertions, feature = "window-timings")))]
118    fn finish(self, _summary: Option<&str>) {}
119}
120
121#[command]
122pub async fn open_window<R: Runtime>(
123    handle: AppHandle<R>,
124    window: Window<R>,
125    registry: State<'_, WindowRegistry>,
126    request: OpenWindowRequest,
127) -> Result<WindowDescriptor, String> {
128    let current_label = window.label();
129    sync_live_window(&handle, &*registry, &current_label)?;
130    open_window_impl(&handle, &*registry, request).await
131}
132
133#[command]
134pub async fn restore_windows<R: Runtime>(
135    handle: AppHandle<R>,
136    registry: State<'_, WindowRegistry>,
137) -> Result<RestoreWindowsResult, String> {
138    let mut timing = OperationTiming::new("restore_windows", None);
139
140    let live_labels: HashSet<String> = registry
141        .list()?
142        .into_iter()
143        .map(|descriptor| descriptor.label)
144        .collect();
145
146    let plan = plan_restore_windows(registry.restore_tracked_windows(), live_labels);
147    timing.mark("planned");
148    let mut restored = Vec::new();
149
150    for descriptor in &plan.restore_order {
151        let request = OpenWindowRequest {
152            label: descriptor.label.clone(),
153            url: Some(descriptor.url.clone()),
154            parent: descriptor.parent.clone(),
155            title: descriptor.title.clone(),
156            geometry: descriptor.geometry.clone(),
157        };
158        open_window_impl(&handle, &*registry, request).await?;
159        restored.push(descriptor.clone());
160    }
161    timing.mark("restored");
162    cleanup_stale_restore_windows(&*registry, &plan)?;
163    timing.mark("cleaned-startup-stale");
164
165    let result = RestoreWindowsResult {
166        restored,
167        already_alive: plan.already_alive,
168        skipped: plan.skipped,
169    };
170    timing.finish(Some(&format!(
171        "restored={} already_alive={} skipped={}",
172        result.restored.len(),
173        result.already_alive.len(),
174        result.skipped.len()
175    )));
176
177    Ok(result)
178}
179
180fn cleanup_stale_restore_windows(
181    registry: &WindowRegistry,
182    plan: &RestorePlan,
183) -> Result<(), String> {
184    let mut stale_labels = HashSet::new();
185
186    for window in &plan.skipped {
187        stale_labels.insert(window.label.clone());
188    }
189
190    // This cleanup only runs during startup restore, so any child still pointing at a
191    // missing parent is stale persisted data rather than a live relationship we should keep.
192    for window in &plan.already_alive_descriptors {
193        if let Some(parent) = window.parent.as_deref() {
194            if !plan.live_labels.contains(parent) {
195                stale_labels.insert(window.label.clone());
196            }
197        }
198    }
199
200    if stale_labels.is_empty() {
201        return Ok(());
202    }
203
204    for label in stale_labels {
205        match registry.purge_persisted_window(&label) {
206            Ok(true) => {}
207            Ok(false) => {}
208            Err(err) => {
209                eprintln!(
210                    "window-system: failed to purge stale window label={} error={err}",
211                    label
212                );
213                return Err(err);
214            }
215        }
216    }
217
218    registry.flush()
219}
220
221struct RestorePlan {
222    restore_order: Vec<WindowDescriptor>,
223    already_alive: Vec<String>,
224    already_alive_descriptors: Vec<WindowDescriptor>,
225    skipped: Vec<RestoreSkippedWindow>,
226    live_labels: HashSet<String>,
227}
228
229fn plan_restore_windows(
230    descriptors: Vec<WindowDescriptor>,
231    mut live_labels: HashSet<String>,
232) -> RestorePlan {
233    let mut pending = descriptors;
234    let mut restore_order = Vec::new();
235    let mut already_alive = Vec::new();
236    let mut already_alive_descriptors = Vec::new();
237    let mut skipped = Vec::new();
238
239    while !pending.is_empty() {
240        let mut next_round = Vec::new();
241        let mut progress = false;
242
243        for descriptor in pending {
244            if live_labels.contains(&descriptor.label) {
245                already_alive.push(descriptor.label.clone());
246                already_alive_descriptors.push(descriptor);
247                continue;
248            }
249
250            if let Some(parent) = descriptor.parent.as_deref() {
251                if !live_labels.contains(parent) {
252                    next_round.push(descriptor);
253                    continue;
254                }
255            }
256
257            live_labels.insert(descriptor.label.clone());
258            restore_order.push(descriptor);
259            progress = true;
260        }
261
262        if !progress {
263            skipped.extend(
264                next_round
265                    .into_iter()
266                    .map(|descriptor| RestoreSkippedWindow {
267                        label: descriptor.label,
268                        parent: descriptor.parent,
269                        reason: RestoreSkipReason::MissingParent,
270                    }),
271            );
272            break;
273        }
274
275        pending = next_round;
276    }
277
278    RestorePlan {
279        restore_order,
280        already_alive,
281        already_alive_descriptors,
282        skipped,
283        live_labels,
284    }
285}
286
287async fn open_window_impl<R: Runtime>(
288    handle: &AppHandle<R>,
289    registry: &WindowRegistry,
290    request: OpenWindowRequest,
291) -> Result<WindowDescriptor, String> {
292    let request = normalize_open_window_request(registry, request)?;
293    let mut timing = OperationTiming::new("open_window", Some(&request.label));
294
295    let reservation = registry.reserve_window(&request.label, request.parent.as_deref())?;
296    timing.mark("reserved");
297
298    let (window, is_reused_window) = build_webview_window(handle, &request)?;
299    timing.mark("built");
300
301    let geometry = request.geometry.clone();
302    if let Some(geometry) = geometry.as_ref() {
303        if let Err(err) = apply_window_geometry(&window, geometry) {
304            if !is_reused_window {
305                let _ = window.destroy();
306            }
307            return Err(err);
308        }
309    }
310    timing.mark("geometry-applied");
311
312    let title = request.title.clone().or_else(|| window.title().ok());
313    let descriptor = WindowDescriptor {
314        label: request.label.clone(),
315        url: request.url.clone(),
316        parent: request.parent.clone(),
317        title,
318        geometry,
319    };
320    if let Err(err) = registry.insert_reserved(descriptor.clone()) {
321        if !is_reused_window {
322            let _ = window.destroy();
323        }
324        return Err(err);
325    }
326    reservation.commit();
327
328    attach_window_event_handlers(handle, registry, &window, &request.label);
329
330    // A reused handle can still be hidden or minimized, so make the reveal path explicit
331    // instead of assuming the runtime window is already visible.
332    if let Err(err) = reveal_window(&window) {
333        let _ = registry.remove(&request.label);
334        if !is_reused_window {
335            let _ = window.destroy();
336        }
337        return Err(err);
338    }
339    timing.mark(if is_reused_window {
340        "reused-shown"
341    } else {
342        "shown"
343    });
344
345    emit_registry_changed(
346        &handle,
347        &registry,
348        RegistryChangeKind::Opened,
349        &request.label,
350    )?;
351    timing.mark("emitted");
352    timing.finish(None);
353
354    Ok(descriptor)
355}
356
357fn normalize_open_window_request(
358    registry: &WindowRegistry,
359    request: OpenWindowRequest,
360) -> Result<NormalizedOpenWindowRequest, String> {
361    let label = normalize_required_text(request.label, "label")?;
362    let parent = normalize_optional_text(request.parent);
363    let title = normalize_optional_text(request.title);
364    let url = normalize_window_url(request.url);
365    let geometry = request
366        .geometry
367        .or_else(|| registry.restore_geometry(&label));
368
369    Ok(NormalizedOpenWindowRequest {
370        label,
371        url,
372        parent,
373        title,
374        geometry,
375    })
376}
377
378fn normalize_required_text(value: String, field_name: &str) -> Result<String, String> {
379    let normalized = value.trim();
380    if normalized.is_empty() {
381        return Err(crate::registry::window_system_error(
382            crate::registry::WindowSystemErrorKind::InvalidLabel,
383            format!("{field_name} is required"),
384        ));
385    }
386
387    Ok(normalized.to_string())
388}
389
390fn normalize_optional_text(value: Option<String>) -> Option<String> {
391    value
392        .map(|value| value.trim().to_string())
393        .filter(|value| !value.is_empty())
394}
395
396fn normalize_window_url(url: Option<String>) -> String {
397    match url {
398        Some(url) => {
399            let normalized = url.trim();
400            if normalized.is_empty() {
401                "index.html".to_string()
402            } else {
403                normalized.to_string()
404            }
405        }
406        None => "index.html".to_string(),
407    }
408}
409
410fn build_webview_window<R: Runtime>(
411    handle: &AppHandle<R>,
412    request: &NormalizedOpenWindowRequest,
413) -> Result<(tauri::WebviewWindow<R>, bool), String> {
414    if let Some(window) = handle.get_webview_window(&request.label) {
415        if let Some(title) = request.title.as_deref() {
416            window.set_title(title).map_err(|err| err.to_string())?;
417        }
418        return Ok((window, true));
419    }
420
421    let mut builder = WebviewWindowBuilder::new(
422        handle,
423        &request.label,
424        WebviewUrl::App(request.url.clone().into()),
425    )
426    .visible(false);
427
428    if let Some(title) = request.title.as_deref() {
429        builder = builder.title(title);
430    }
431
432    let builder = attach_native_parent(handle, builder, request.parent.as_deref())?;
433    builder
434        .build()
435        .map(|window| (window, false))
436        .map_err(|err| err.to_string())
437}
438
439fn reveal_window<R: Runtime>(window: &tauri::WebviewWindow<R>) -> Result<(), String> {
440    if let Ok(true) = window.is_minimized() {
441        window.unminimize().map_err(|err| err.to_string())?;
442    }
443
444    window.show().map_err(|err| err.to_string())?;
445
446    if let Err(err) = window.set_focus() {
447        eprintln!(
448            "window-system: failed to focus window label={} error={err}",
449            window.label()
450        );
451    }
452
453    Ok(())
454}
455
456#[cfg(any(debug_assertions, feature = "window-timings"))]
457fn log_operation_timing(
458    operation: &str,
459    subject: Option<&str>,
460    stage: &str,
461    stage_ms: u128,
462    total_ms: u128,
463) {
464    let subject = subject.unwrap_or("-");
465
466    #[cfg(debug_assertions)]
467    eprintln!(
468        "window-system: operation={} subject={} stage={} stage_ms={} total_ms={}",
469        operation, subject, stage, stage_ms, total_ms
470    );
471
472    #[cfg(all(not(debug_assertions), feature = "window-timings"))]
473    tracing::debug!(
474        operation = operation,
475        subject = subject,
476        stage = stage,
477        stage_ms = stage_ms,
478        total_ms = total_ms,
479        "window-system timing"
480    );
481}
482
483#[cfg(any(debug_assertions, feature = "window-timings"))]
484fn log_operation_completion(
485    operation: &str,
486    subject: Option<&str>,
487    total_ms: u128,
488    summary: Option<&str>,
489) {
490    let subject = subject.unwrap_or("-");
491
492    #[cfg(debug_assertions)]
493    match summary {
494        Some(summary) => eprintln!(
495            "window-system: operation={} subject={} stage=complete total_ms={} {}",
496            operation, subject, total_ms, summary
497        ),
498        None => eprintln!(
499            "window-system: operation={} subject={} stage=complete total_ms={}",
500            operation, subject, total_ms
501        ),
502    }
503
504    #[cfg(all(not(debug_assertions), feature = "window-timings"))]
505    match summary {
506        Some(summary) => tracing::debug!(
507            operation = operation,
508            subject = subject,
509            total_ms = total_ms,
510            summary = summary,
511            "window-system operation complete"
512        ),
513        None => tracing::debug!(
514            operation = operation,
515            subject = subject,
516            total_ms = total_ms,
517            "window-system operation complete"
518        ),
519    }
520}
521
522#[command]
523pub async fn close_window<R: Runtime>(
524    handle: AppHandle<R>,
525    registry: State<'_, WindowRegistry>,
526    label: String,
527) -> Result<(), String> {
528    if let Err(err) = close_window_tree_impl(&handle, &*registry, &label, false).await {
529        eprintln!(
530            "{}",
531            crate::lifecycle::format_teardown_failure("command-close", &label, &err)
532        );
533        return Err(err);
534    }
535
536    Ok(())
537}
538
539#[command]
540pub fn list_windows(registry: State<'_, WindowRegistry>) -> Result<Vec<WindowDescriptor>, String> {
541    registry.list()
542}
543
544#[command]
545pub fn emit_to_window<R: Runtime>(
546    handle: AppHandle<R>,
547    label: String,
548    event: String,
549    payload: Value,
550) -> Result<(), String> {
551    crate::events::emit_to_window(&handle, &label, &event, payload)
552}
553
554#[command]
555pub fn send_window_message<R: Runtime>(
556    window: Window<R>,
557    handle: AppHandle<R>,
558    registry: State<'_, WindowRegistry>,
559    request: SendWindowMessageRequest,
560) -> Result<WindowMessageDispatchResult, String> {
561    send_window_message_impl(window, handle, registry, request)
562}
563
564#[command]
565pub fn broadcast_window_message<R: Runtime>(
566    window: Window<R>,
567    handle: AppHandle<R>,
568    registry: State<'_, WindowRegistry>,
569    request: BroadcastWindowMessageRequest,
570) -> Result<WindowMessageDispatchResult, String> {
571    broadcast_window_message_impl(window, handle, registry, request)
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577    use crate::registry::WindowStateStore;
578    use serde_json::json;
579    use std::collections::HashSet;
580    use std::path::PathBuf;
581    use std::time::{SystemTime, UNIX_EPOCH};
582
583    fn descriptor(label: &str, parent: Option<&str>) -> WindowDescriptor {
584        WindowDescriptor {
585            label: label.into(),
586            url: format!("{label}.html"),
587            parent: parent.map(str::to_string),
588            title: None,
589            geometry: None,
590        }
591    }
592
593    fn unique_temp_path(name: &str) -> PathBuf {
594        let suffix = SystemTime::now()
595            .duration_since(UNIX_EPOCH)
596            .expect("system clock should be monotonic for tests")
597            .as_nanos();
598        std::env::temp_dir().join(format!("tauri-window-system-commands-{name}-{suffix}.json"))
599    }
600
601    #[test]
602    fn restore_plan_is_parent_first() {
603        let plan = plan_restore_windows(
604            vec![descriptor("child", Some("main")), descriptor("main", None)],
605            HashSet::new(),
606        );
607
608        assert_eq!(
609            plan.restore_order
610                .iter()
611                .map(|descriptor| descriptor.label.as_str())
612                .collect::<Vec<_>>(),
613            vec!["main", "child"]
614        );
615        assert!(plan.already_alive.is_empty());
616        assert!(plan.skipped.is_empty());
617    }
618
619    #[test]
620    fn restore_plan_marks_existing_windows_as_alive() {
621        let mut live = HashSet::new();
622        live.insert("main".to_string());
623
624        let plan = plan_restore_windows(
625            vec![descriptor("main", None), descriptor("child", Some("main"))],
626            live,
627        );
628
629        assert_eq!(plan.already_alive, vec!["main".to_string()]);
630        assert_eq!(
631            plan.restore_order
632                .iter()
633                .map(|descriptor| descriptor.label.as_str())
634                .collect::<Vec<_>>(),
635            vec!["child"]
636        );
637    }
638
639    #[test]
640    fn restore_plan_skips_unresolved_children() {
641        let plan = plan_restore_windows(vec![descriptor("child", Some("missing"))], HashSet::new());
642
643        assert!(plan.restore_order.is_empty());
644        assert!(plan.already_alive.is_empty());
645        assert_eq!(plan.skipped.len(), 1);
646        assert_eq!(plan.skipped[0].label, "child");
647    }
648
649    #[test]
650    fn cleanup_skipped_restore_windows_removes_persisted_state() {
651        let path = unique_temp_path("cleanup");
652        let store = WindowStateStore::new(path.clone());
653        let registry = WindowRegistry::new(store.clone());
654        let geometry = WindowGeometry {
655            x: 10.0,
656            y: 20.0,
657            width: 30.0,
658            height: 40.0,
659        };
660
661        store
662            .remember("child", geometry.clone())
663            .expect("child geometry should persist");
664        store
665            .remember_window(WindowDescriptor {
666                label: "child".into(),
667                url: "child.html".into(),
668                parent: Some("missing".into()),
669                title: Some("Child".into()),
670                geometry: Some(geometry),
671            })
672            .expect("child descriptor should persist");
673
674        let plan = RestorePlan {
675            restore_order: Vec::new(),
676            already_alive: Vec::new(),
677            already_alive_descriptors: Vec::new(),
678            skipped: vec![RestoreSkippedWindow {
679                label: "child".into(),
680                parent: Some("missing".into()),
681                reason: RestoreSkipReason::MissingParent,
682            }],
683            live_labels: HashSet::new(),
684        };
685
686        cleanup_stale_restore_windows(&registry, &plan).expect("cleanup should flush");
687
688        assert!(registry
689            .restore_tracked_windows()
690            .iter()
691            .all(|window| window.label != "child"));
692        assert_eq!(registry.restore_geometry("child"), None);
693
694        let restored_store = WindowStateStore::new(path);
695        assert!(restored_store
696            .restore_tracked_windows()
697            .iter()
698            .all(|window| window.label != "child"));
699        assert_eq!(restored_store.restore("child"), None);
700    }
701
702    #[test]
703    fn cleanup_stale_restore_windows_removes_orphaned_alive_child() {
704        let path = unique_temp_path("orphan-alive");
705        let store = WindowStateStore::new(path.clone());
706        let registry = WindowRegistry::new(store.clone());
707        let descriptor = WindowDescriptor {
708            label: "child".into(),
709            url: "child.html".into(),
710            parent: Some("missing".into()),
711            title: Some("Child".into()),
712            geometry: None,
713        };
714
715        store
716            .remember_window(descriptor.clone())
717            .expect("tracked child should persist");
718        store.flush().expect("snapshot should flush");
719
720        let mut live_labels = HashSet::new();
721        live_labels.insert("child".to_string());
722
723        let plan = RestorePlan {
724            restore_order: Vec::new(),
725            already_alive: vec!["child".into()],
726            already_alive_descriptors: vec![descriptor],
727            skipped: Vec::new(),
728            live_labels,
729        };
730
731        cleanup_stale_restore_windows(&registry, &plan).expect("cleanup should flush");
732
733        let restored_store = WindowStateStore::new(path);
734        assert!(restored_store.restore_tracked_windows().is_empty());
735    }
736
737    #[test]
738    fn normalize_open_window_request_trims_and_restores_geometry() {
739        let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("normalize")));
740        let geometry = WindowGeometry {
741            x: 10.0,
742            y: 20.0,
743            width: 300.0,
744            height: 200.0,
745        };
746
747        registry
748            .insert(descriptor("main", None))
749            .expect("main should insert");
750        registry
751            .remember_geometry("main", geometry.clone())
752            .expect("geometry should persist");
753
754        let normalized = normalize_open_window_request(
755            &registry,
756            OpenWindowRequest {
757                label: " main ".into(),
758                url: Some(" child.html ".into()),
759                parent: Some(" root ".into()),
760                title: Some(" Child Window ".into()),
761                geometry: None,
762            },
763        )
764        .expect("request should normalize");
765
766        assert_eq!(normalized.label, "main");
767        assert_eq!(normalized.url, "child.html");
768        assert_eq!(normalized.parent, Some("root".into()));
769        assert_eq!(normalized.title, Some("Child Window".into()));
770        assert_eq!(normalized.geometry, Some(geometry));
771    }
772
773    #[test]
774    fn normalize_open_window_request_rejects_blank_label() {
775        let registry = WindowRegistry::new(WindowStateStore::new(unique_temp_path("invalid")));
776
777        let error = normalize_open_window_request(
778            &registry,
779            OpenWindowRequest {
780                label: "   ".into(),
781                url: None,
782                parent: None,
783                title: None,
784                geometry: None,
785            },
786        )
787        .expect_err("blank label should be rejected");
788
789        assert!(error.contains("invalid-label"));
790        assert!(error.contains("label is required"));
791    }
792
793    #[test]
794    fn restore_windows_result_serializes_with_stable_field_names() {
795        let result = RestoreWindowsResult {
796            restored: vec![descriptor("main", None)],
797            already_alive: vec!["child".into()],
798            skipped: vec![RestoreSkippedWindow {
799                label: "orphan".into(),
800                parent: Some("missing".into()),
801                reason: RestoreSkipReason::MissingParent,
802            }],
803        };
804
805        let value = serde_json::to_value(result).expect("result should serialize");
806
807        assert_eq!(value["restored"][0]["label"], "main");
808        assert_eq!(value["alreadyAlive"], json!(["child"]));
809        assert_eq!(value["skipped"][0]["reason"], "missing-parent");
810        assert_eq!(value["skipped"][0]["parent"], "missing");
811    }
812}