1use std::{
2 borrow::Cow,
3 collections::BTreeMap,
4 sync::{Arc, Mutex},
5 thread,
6};
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11 HOVERFX_PACKAGE_NAME, HOVERFX_PACKAGE_VERSION, HoverFxConfig, HoverFxDiagnosticVerbosity,
12 HoverFxFallbackStrategy, HoverFxManifestFragment, HoverFxOutputReport, HoverFxPreset,
13 HoverFxRoutePolicy, HoverFxSerializationFormat, explain_hoverfx, hoverfx_cache_key,
14 hoverfx_manifest_fragment,
15};
16
17pub const DEFAULT_HOVERFX_CONFIG_ID: &str = "__DXH_CONFIG__";
18pub const DEFAULT_HOVERFX_PREPAINT_STYLE_ID: &str = "__DXH_PREPAINT__";
19pub const DEFAULT_HOVERFX_RUNTIME_ASSET_ID: &str = "hoverfx.runtime";
20pub const DEFAULT_HOVERFX_WORKER_ASSET_ID: &str = "hoverfx.worker";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "kebab-case")]
24pub enum HoverFxDiagnosticContext {
25 Build,
26 Ssr,
27 Runtime,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct HoverFxDiagnosticVerbosityByContext {
33 pub build: HoverFxDiagnosticVerbosity,
34 pub ssr: HoverFxDiagnosticVerbosity,
35 pub runtime: HoverFxDiagnosticVerbosity,
36}
37
38impl Default for HoverFxDiagnosticVerbosityByContext {
39 fn default() -> Self {
40 Self {
41 build: HoverFxDiagnosticVerbosity::Detailed,
42 ssr: HoverFxDiagnosticVerbosity::Summary,
43 runtime: HoverFxDiagnosticVerbosity::Off,
44 }
45 }
46}
47
48impl HoverFxDiagnosticVerbosityByContext {
49 pub fn for_context(&self, context: HoverFxDiagnosticContext) -> HoverFxDiagnosticVerbosity {
50 match context {
51 HoverFxDiagnosticContext::Build => self.build,
52 HoverFxDiagnosticContext::Ssr => self.ssr,
53 HoverFxDiagnosticContext::Runtime => self.runtime,
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct HoverFxRuntimeIds {
61 pub config_id: String,
62 pub prepaint_style_id: String,
63 pub runtime_asset_id: String,
64 pub worker_asset_id: String,
65}
66
67impl Default for HoverFxRuntimeIds {
68 fn default() -> Self {
69 Self {
70 config_id: DEFAULT_HOVERFX_CONFIG_ID.to_string(),
71 prepaint_style_id: DEFAULT_HOVERFX_PREPAINT_STYLE_ID.to_string(),
72 runtime_asset_id: DEFAULT_HOVERFX_RUNTIME_ASSET_ID.to_string(),
73 worker_asset_id: DEFAULT_HOVERFX_WORKER_ASSET_ID.to_string(),
74 }
75 }
76}
77
78impl HoverFxRuntimeIds {
79 pub fn with_config_id(mut self, id: impl Into<String>) -> Self {
80 self.config_id = id.into();
81 self
82 }
83
84 pub fn with_prepaint_style_id(mut self, id: impl Into<String>) -> Self {
85 self.prepaint_style_id = id.into();
86 self
87 }
88
89 pub fn with_runtime_asset_id(mut self, id: impl Into<String>) -> Self {
90 self.runtime_asset_id = id.into();
91 self
92 }
93
94 pub fn with_worker_asset_id(mut self, id: impl Into<String>) -> Self {
95 self.worker_asset_id = id.into();
96 self
97 }
98
99 pub fn duplicate_ids(&self) -> Vec<String> {
100 let mut seen = BTreeMap::<&str, usize>::new();
101 for id in [
102 self.config_id.as_str(),
103 self.prepaint_style_id.as_str(),
104 self.runtime_asset_id.as_str(),
105 self.worker_asset_id.as_str(),
106 ] {
107 *seen.entry(id).or_default() += 1;
108 }
109 seen.into_iter()
110 .filter_map(|(id, count)| (count > 1).then(|| id.to_string()))
111 .collect()
112 }
113
114 pub fn deduped(mut self) -> Self {
115 let mut counts = BTreeMap::<String, usize>::new();
116 for id in [
117 &mut self.config_id,
118 &mut self.prepaint_style_id,
119 &mut self.runtime_asset_id,
120 &mut self.worker_asset_id,
121 ] {
122 let count = counts.entry(id.clone()).or_default();
123 if *count > 0 {
124 id.push('-');
125 id.push_str(&(*count + 1).to_string());
126 }
127 *count += 1;
128 }
129 self
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct HoverFxAssetPaths {
136 pub runtime_base_path: String,
137 pub worker_base_path: String,
138 pub runtime_asset_name: String,
139 pub worker_asset_name: String,
140}
141
142impl Default for HoverFxAssetPaths {
143 fn default() -> Self {
144 Self {
145 runtime_base_path: "/assets".to_string(),
146 worker_base_path: "/assets".to_string(),
147 runtime_asset_name: "dioxus-hoverfx.js".to_string(),
148 worker_asset_name: "dioxus-hoverfx-worker.js".to_string(),
149 }
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "kebab-case")]
155pub enum HoverFxAssetBudgetCategory {
156 RuntimeScript,
157 WorkerScript,
158 PrepaintStyle,
159 ConfigJson,
160 EffectCssVars,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct HoverFxAssetBudgetEntry {
166 pub category: HoverFxAssetBudgetCategory,
167 pub bytes: usize,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct HoverFxAssetBudgetBridge {
173 pub package: String,
174 pub entries: Vec<HoverFxAssetBudgetEntry>,
175}
176
177impl HoverFxAssetBudgetBridge {
178 pub fn total_bytes(&self) -> usize {
179 self.entries.iter().map(|entry| entry.bytes).sum()
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct HoverFxBatchOptions {
186 pub format: HoverFxSerializationFormat,
187 pub deterministic_parallel: bool,
188 pub sort_by_cache_key: bool,
189}
190
191impl Default for HoverFxBatchOptions {
192 fn default() -> Self {
193 Self {
194 format: HoverFxSerializationFormat::StableJson,
195 deterministic_parallel: false,
196 sort_by_cache_key: true,
197 }
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct HoverFxBatchPayload {
204 pub route: Option<String>,
205 pub cache_key: String,
206 pub json: String,
207 pub bytes: usize,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct HoverFxBatchSerializationReport {
213 pub package: String,
214 pub deterministic_parallel: bool,
215 pub payloads: Vec<HoverFxBatchPayload>,
216 pub total_bytes: usize,
217}
218
219#[derive(Debug)]
220pub struct HoverFxBorrowedView<'a> {
221 pub config: &'a HoverFxConfig,
222 pub policy: Cow<'a, HoverFxRoutePolicy>,
223}
224
225impl<'a> HoverFxBorrowedView<'a> {
226 pub fn new(config: &'a HoverFxConfig, policy: &'a HoverFxRoutePolicy) -> Self {
227 Self {
228 config,
229 policy: Cow::Borrowed(policy),
230 }
231 }
232
233 pub fn with_owned_policy(config: &'a HoverFxConfig, policy: HoverFxRoutePolicy) -> Self {
234 Self {
235 config,
236 policy: Cow::Owned(policy),
237 }
238 }
239
240 pub fn manifest_fragment(&self) -> HoverFxManifestFragment {
241 hoverfx_manifest_fragment(self.config, self.policy.as_ref())
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct HoverFxCssFragment {
248 pub id: String,
249 pub css: String,
250 pub bytes: usize,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(rename_all = "camelCase")]
255pub struct HoverFxMotionPolicy {
256 pub reduced_motion: String,
257 pub view_transition: String,
258 pub render_lane: String,
259}
260
261impl Default for HoverFxMotionPolicy {
262 fn default() -> Self {
263 Self {
264 reduced_motion: "respect".to_string(),
265 view_transition: "optional".to_string(),
266 render_lane: "background".to_string(),
267 }
268 }
269}
270
271pub trait HoverFxMotionPolicyHook {
272 fn apply(&self, policy: HoverFxMotionPolicy) -> HoverFxMotionPolicy;
273}
274
275#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct HoverFxRouteMetadata {
278 pub route: String,
279 #[serde(default, skip_serializing_if = "Vec::is_empty")]
280 pub cache_keys: Vec<String>,
281 #[serde(default, skip_serializing_if = "Vec::is_empty")]
282 pub packages: Vec<String>,
283 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
284 pub labels: BTreeMap<String, String>,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct HoverFxOptimizerArtifact {
290 pub id: String,
291 pub content_hash: String,
292 pub bytes: usize,
293 pub minified: bool,
294}
295
296pub trait HoverFxOptimizerReuseHook {
297 fn reuse_artifact(&self, artifact: &HoverFxOptimizerArtifact) -> Option<String>;
298}
299
300pub trait HoverFxCacheBackend {
301 fn get(&self, key: &str) -> Option<String>;
302 fn put(&self, key: String, value: String);
303}
304
305#[derive(Clone, Default)]
306pub struct HoverFxMemoryCache {
307 values: Arc<Mutex<BTreeMap<String, String>>>,
308}
309
310impl HoverFxCacheBackend for HoverFxMemoryCache {
311 fn get(&self, key: &str) -> Option<String> {
312 self.values.lock().ok()?.get(key).cloned()
313 }
314
315 fn put(&self, key: String, value: String) {
316 if let Ok(mut values) = self.values.lock() {
317 values.insert(key, value);
318 }
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct HoverFxOffloadPlan {
325 pub package: String,
326 pub lane: String,
327 pub serializable: bool,
328 pub fallback: HoverFxFallbackStrategy,
329 pub payload_cache_key: String,
330 pub tasks: Vec<String>,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct HoverFxCompactDictionary {
336 pub version: u8,
337 pub terms: BTreeMap<String, u16>,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct HoverFxTraceEvent {
343 pub name: String,
344 pub cache_key: String,
345 pub route: Option<String>,
346 pub message: String,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct HoverFxStrataMigrationPlan {
352 pub package: String,
353 pub route: Option<String>,
354 pub steps: Vec<String>,
355}
356
357pub fn hoverfx_serialize_batch<'a>(
371 entries: impl IntoIterator<Item = (&'a HoverFxConfig, HoverFxRoutePolicy)>,
372 options: HoverFxBatchOptions,
373) -> serde_json::Result<HoverFxBatchSerializationReport> {
374 let entries = entries.into_iter().collect::<Vec<_>>();
375 let mut payloads = if options.deterministic_parallel && entries.len() > 1 {
376 thread::scope(|scope| {
377 let mut handles = Vec::new();
378 for (config, policy) in entries {
379 handles.push(scope.spawn(move || serialize_one(config, &policy, options.format)));
380 }
381 handles
382 .into_iter()
383 .map(|handle| handle.join().expect("HoverFX batch worker panicked"))
384 .collect::<serde_json::Result<Vec<_>>>()
385 })?
386 } else {
387 entries
388 .into_iter()
389 .map(|(config, policy)| serialize_one(config, &policy, options.format))
390 .collect::<serde_json::Result<Vec<_>>>()?
391 };
392 if options.sort_by_cache_key {
393 payloads.sort_by(|a, b| a.cache_key.cmp(&b.cache_key));
394 }
395 let total_bytes = payloads.iter().map(|payload| payload.bytes).sum();
396 Ok(HoverFxBatchSerializationReport {
397 package: HOVERFX_PACKAGE_NAME.to_string(),
398 deterministic_parallel: options.deterministic_parallel,
399 payloads,
400 total_bytes,
401 })
402}
403
404pub fn hoverfx_asset_budget_bridge(
405 config: &HoverFxConfig,
406 policy: &HoverFxRoutePolicy,
407) -> HoverFxAssetBudgetBridge {
408 let output = config.output_report(policy);
409 HoverFxAssetBudgetBridge {
410 package: HOVERFX_PACKAGE_NAME.to_string(),
411 entries: vec![
412 HoverFxAssetBudgetEntry {
413 category: HoverFxAssetBudgetCategory::ConfigJson,
414 bytes: output.config_bytes,
415 },
416 HoverFxAssetBudgetEntry {
417 category: HoverFxAssetBudgetCategory::RuntimeScript,
418 bytes: output.runtime_bytes,
419 },
420 HoverFxAssetBudgetEntry {
421 category: HoverFxAssetBudgetCategory::EffectCssVars,
422 bytes: output.style_bytes,
423 },
424 ],
425 }
426}
427
428pub fn hoverfx_precomputed_css_fragments() -> Vec<HoverFxCssFragment> {
429 HoverFxPreset::ALL
430 .into_iter()
431 .map(|preset| {
432 let css = format!(
433 "[data-dxh-effect=\"{}\"]{{--dxh-preset:\"{}\";}}",
434 preset.as_attr(),
435 preset.as_attr()
436 );
437 HoverFxCssFragment {
438 id: preset.as_attr().to_string(),
439 bytes: css.len(),
440 css,
441 }
442 })
443 .collect()
444}
445
446pub fn hoverfx_apply_motion_hook<H: HoverFxMotionPolicyHook>(
447 policy: HoverFxMotionPolicy,
448 hook: &H,
449) -> HoverFxMotionPolicy {
450 hook.apply(policy)
451}
452
453pub fn hoverfx_coalesce_route_metadata(
454 route: impl Into<String>,
455 fragments: impl IntoIterator<Item = HoverFxManifestFragment>,
456) -> HoverFxRouteMetadata {
457 let mut metadata = HoverFxRouteMetadata {
458 route: route.into(),
459 ..HoverFxRouteMetadata::default()
460 };
461 for fragment in fragments {
462 metadata.cache_keys.push(fragment.cache_key);
463 if !metadata.packages.contains(&fragment.package) {
464 metadata.packages.push(fragment.package);
465 }
466 metadata.labels.extend(fragment.labels);
467 }
468 metadata.cache_keys.sort();
469 metadata.packages.sort();
470 metadata
471}
472
473pub fn hoverfx_optimizer_artifacts(report: &HoverFxOutputReport) -> Vec<HoverFxOptimizerArtifact> {
474 [
475 ("hoverfx.config", report.config_bytes, true),
476 ("hoverfx.runtime", report.runtime_bytes, true),
477 ("hoverfx.style", report.style_bytes, true),
478 ]
479 .into_iter()
480 .map(|(id, bytes, minified)| HoverFxOptimizerArtifact {
481 id: id.to_string(),
482 content_hash: integration_hash_hex([id, &report.cache_key, &bytes.to_string()]),
483 bytes,
484 minified,
485 })
486 .collect()
487}
488
489pub fn hoverfx_cache_put_report<B: HoverFxCacheBackend>(
490 cache: &B,
491 report: &HoverFxOutputReport,
492) -> String {
493 let key = format!(
494 "{}:{}:{}",
495 HOVERFX_PACKAGE_NAME, HOVERFX_PACKAGE_VERSION, report.cache_key
496 );
497 cache.put(
498 key.clone(),
499 serde_json::to_string(report).unwrap_or_default(),
500 );
501 key
502}
503
504pub fn hoverfx_workertown_offload_plan(
505 config: &HoverFxConfig,
506 policy: &HoverFxRoutePolicy,
507) -> HoverFxOffloadPlan {
508 HoverFxOffloadPlan {
509 package: HOVERFX_PACKAGE_NAME.to_string(),
510 lane: "hoverfx".to_string(),
511 serializable: true,
512 fallback: policy.fallback,
513 payload_cache_key: hoverfx_cache_key(config, policy.route.as_deref(), Some("workertown")),
514 tasks: vec![
515 "pointer-sampling".to_string(),
516 "dirty-rect-render".to_string(),
517 "tooltip-textfx-trigger".to_string(),
518 ],
519 }
520}
521
522pub fn hoverfx_compact_dictionary() -> HoverFxCompactDictionary {
523 let mut terms = BTreeMap::new();
524 for (index, term) in [
525 "config",
526 "runtime",
527 "worker",
528 "style",
529 "route",
530 "profile",
531 "fallback",
532 "workertown",
533 ]
534 .into_iter()
535 .enumerate()
536 {
537 terms.insert(term.to_string(), index as u16);
538 }
539 HoverFxCompactDictionary { version: 1, terms }
540}
541
542pub fn hoverfx_trace_report(
543 config: &HoverFxConfig,
544 policy: &HoverFxRoutePolicy,
545 mut emit: impl FnMut(HoverFxTraceEvent),
546) {
547 let explain = explain_hoverfx(config, policy);
548 emit(HoverFxTraceEvent {
549 name: "hoverfx.policy".to_string(),
550 cache_key: explain.cache_key.clone(),
551 route: policy.route.clone(),
552 message: explain.runtime_decision,
553 });
554 emit(HoverFxTraceEvent {
555 name: "hoverfx.output".to_string(),
556 cache_key: explain.cache_key,
557 route: policy.route.clone(),
558 message: format!("{} config bytes", explain.output.config_bytes),
559 });
560}
561
562pub fn hoverfx_strata_migration_plan(
563 config: &HoverFxConfig,
564 policy: &HoverFxRoutePolicy,
565) -> HoverFxStrataMigrationPlan {
566 HoverFxStrataMigrationPlan {
567 package: HOVERFX_PACKAGE_NAME.to_string(),
568 route: policy.route.clone(),
569 steps: vec![
570 "emit HoverFxManifestFragment from standalone config".to_string(),
571 format!(
572 "register cache key {}",
573 config.cache_key(policy.route.as_deref())
574 ),
575 "attach runtime/style assets through Strata route metadata".to_string(),
576 "preserve fallback and diagnostic context policy".to_string(),
577 ],
578 }
579}
580
581pub fn hoverfx_conformance_fixture() -> serde_json::Value {
582 let config = HoverFxConfig::default();
583 let policy = HoverFxRoutePolicy::default().route("/hoverfx");
584 serde_json::json!({
585 "manifest": hoverfx_manifest_fragment(&config, &policy),
586 "dictionary": hoverfx_compact_dictionary(),
587 "cssFragments": hoverfx_precomputed_css_fragments(),
588 })
589}
590
591fn serialize_one(
592 config: &HoverFxConfig,
593 policy: &HoverFxRoutePolicy,
594 format: HoverFxSerializationFormat,
595) -> serde_json::Result<HoverFxBatchPayload> {
596 let json = config.to_preferred_json(format)?;
597 Ok(HoverFxBatchPayload {
598 route: policy.route.clone(),
599 cache_key: hoverfx_cache_key(config, policy.route.as_deref(), Some(format.as_attr())),
600 bytes: json.len(),
601 json,
602 })
603}
604
605fn integration_hash_hex<'a>(parts: impl IntoIterator<Item = &'a str>) -> String {
606 let mut hash = 0xcbf29ce484222325u64;
607 for part in parts {
608 for byte in part.as_bytes() {
609 hash ^= u64::from(*byte);
610 hash = hash.wrapping_mul(0x100000001b3);
611 }
612 hash ^= 0xff;
613 hash = hash.wrapping_mul(0x100000001b3);
614 }
615 format!("{hash:016x}")
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use crate::{HoverFxDefinition, HoverFxPresetProfile, hoverfx_output_budget};
622
623 #[test]
624 fn batch_serialization_parallel_is_deterministic() {
625 let config = HoverFxConfig::default()
626 .with_effect(HoverFxDefinition::from_preset(HoverFxPreset::Tooltip));
627 let a = HoverFxRoutePolicy::default().route("/a");
628 let b = HoverFxRoutePolicy::default().route("/b");
629 let serial = hoverfx_serialize_batch(
630 [(&config, a.clone()), (&config, b.clone())],
631 HoverFxBatchOptions::default(),
632 )
633 .unwrap();
634 let parallel = hoverfx_serialize_batch(
635 [(&config, a), (&config, b)],
636 HoverFxBatchOptions {
637 deterministic_parallel: true,
638 ..HoverFxBatchOptions::default()
639 },
640 )
641 .unwrap();
642
643 assert_eq!(serial.payloads, parallel.payloads);
644 assert!(parallel.deterministic_parallel);
645 }
646
647 #[test]
648 fn asset_budget_duplicate_ids_cache_and_offload_are_reported() {
649 let config = HoverFxConfig::default().with_profile(HoverFxPresetProfile::Aggressive);
650 let policy = HoverFxRoutePolicy::default()
651 .route("/hoverfx")
652 .budget(hoverfx_output_budget().runtime_bytes(8));
653 let report = config.output_report(&policy);
654 let bridge = hoverfx_asset_budget_bridge(&config, &policy);
655 let ids = HoverFxRuntimeIds::default().with_config_id(DEFAULT_HOVERFX_PREPAINT_STYLE_ID);
656 let cache = HoverFxMemoryCache::default();
657 let key = hoverfx_cache_put_report(&cache, &report);
658 let offload = hoverfx_workertown_offload_plan(&config, &policy);
659
660 assert!(bridge.total_bytes() >= report.config_bytes);
661 assert_eq!(
662 ids.duplicate_ids(),
663 vec![DEFAULT_HOVERFX_PREPAINT_STYLE_ID.to_string()]
664 );
665 assert!(ids.deduped().duplicate_ids().is_empty());
666 assert!(cache.get(&key).is_some());
667 assert!(offload.serializable);
668 assert!(offload.tasks.iter().any(|task| task == "dirty-rect-render"));
669 }
670
671 #[test]
672 fn css_motion_metadata_optimizer_trace_and_migration_are_concrete() {
673 struct ForceDisable;
674
675 impl HoverFxMotionPolicyHook for ForceDisable {
676 fn apply(&self, mut policy: HoverFxMotionPolicy) -> HoverFxMotionPolicy {
677 policy.reduced_motion = "disable".to_string();
678 policy
679 }
680 }
681
682 let config = HoverFxConfig::default();
683 let policy = HoverFxRoutePolicy::default().route("/hoverfx");
684 let manifest = config.manifest_fragment(&policy);
685 let metadata = hoverfx_coalesce_route_metadata("/hoverfx", [manifest]);
686 let css = hoverfx_precomputed_css_fragments();
687 let output = config.output_report(&policy);
688 let artifacts = hoverfx_optimizer_artifacts(&output);
689 let motion = hoverfx_apply_motion_hook(HoverFxMotionPolicy::default(), &ForceDisable);
690 let dictionary = hoverfx_compact_dictionary();
691 let migration = hoverfx_strata_migration_plan(&config, &policy);
692 let mut traces = Vec::new();
693 hoverfx_trace_report(&config, &policy, |event| traces.push(event));
694 let fixture = hoverfx_conformance_fixture();
695
696 assert_eq!(metadata.route, "/hoverfx");
697 assert!(css.iter().any(|fragment| fragment.id == "tooltip"));
698 assert!(
699 artifacts
700 .iter()
701 .any(|artifact| artifact.id == "hoverfx.runtime")
702 );
703 assert_eq!(motion.reduced_motion, "disable");
704 assert_eq!(dictionary.terms["runtime"], 1);
705 assert!(migration.steps.iter().any(|step| step.contains("Strata")));
706 assert_eq!(traces.len(), 2);
707 assert!(fixture.get("manifest").is_some());
708 }
709}