@@ -16724,4 +16724,142 @@ const composed = stylex.create({ combined: { ...stylex.include(base.root) } });"
1672416724 .unwrap()
1672516725 ));
1672616726 }
16727+
16728+ #[test]
16729+ #[serial]
16730+ fn extract_pseudo_selector_with_non_literal_value_graceful() {
16731+ // Minimal regression tests for the panic that ERROR.tsx surfaced.
16732+ //
16733+ // When a pseudo-selector prop (`_hover`, `_active`, `_focus`, ...)
16734+ // receives anything other than an inline object literal, the
16735+ // extractor used to `unwrap()` a None prop-name inside the
16736+ // `_xxx` selector recursion path and panic. The fix is to return
16737+ // an empty ExtractResult from the non-literal branches of
16738+ // `extract_style_from_expression` and
16739+ // `extract_style_from_member_expression` when `name` is `None`.
16740+ //
16741+ // Each case below must:
16742+ // 1. NOT panic (used to panic before the fix)
16743+ // 2. Return Ok from `extract()`
16744+ // 3. Drop the un-extractable pseudo-selector attribute from the
16745+ // generated code (no class, no runtime style — devup-ui is
16746+ // fully static, so there is no runtime fallback to fall back
16747+ // to)
16748+ let cases: &[(&str, &str)] = &[
16749+ (
16750+ "identifier",
16751+ r#"import {Box} from '@devup-ui/react'
16752+ const hoverStyle = { opacity: 1 };
16753+ export const A = () => <Box _hover={hoverStyle} />;
16754+ "#,
16755+ ),
16756+ (
16757+ "call expression",
16758+ r#"import {Box} from '@devup-ui/react'
16759+ declare const getHover: () => object;
16760+ export const A = () => <Box _hover={getHover()} />;
16761+ "#,
16762+ ),
16763+ (
16764+ "member expression",
16765+ r#"import {Box} from '@devup-ui/react'
16766+ declare const styles: { hover: object };
16767+ export const A = () => <Box _hover={styles.hover} />;
16768+ "#,
16769+ ),
16770+ (
16771+ "binary expression",
16772+ r#"import {Box} from '@devup-ui/react'
16773+ declare const a: any; declare const b: any;
16774+ export const A = () => <Box _hover={a || b} />;
16775+ "#,
16776+ ),
16777+ (
16778+ "template literal",
16779+ r#"import {Box} from '@devup-ui/react'
16780+ declare const x: string;
16781+ export const A = () => <Box _hover={`${x}`} />;
16782+ "#,
16783+ ),
16784+ (
16785+ "unary expression",
16786+ r#"import {Box} from '@devup-ui/react'
16787+ declare const v: any;
16788+ export const A = () => <Box _hover={!v} />;
16789+ "#,
16790+ ),
16791+ (
16792+ "computed member expression (array index)",
16793+ r#"import {Box} from '@devup-ui/react'
16794+ declare const arr: any[];
16795+ export const A = () => <Box _hover={arr[0]} />;
16796+ "#,
16797+ ),
16798+ ];
16799+
16800+ for (label, src) in cases {
16801+ reset_class_map();
16802+ reset_file_map();
16803+ let result = std::panic::catch_unwind(|| {
16804+ extract(
16805+ "test.tsx",
16806+ src,
16807+ ExtractOption {
16808+ package: "@devup-ui/react".to_string(),
16809+ css_dir: "@devup-ui/react".to_string(),
16810+ single_css: true,
16811+ import_main_css: false,
16812+ import_aliases: HashMap::new(),
16813+ },
16814+ )
16815+ });
16816+ match result {
16817+ Ok(Ok(output)) => {
16818+ println!(
16819+ "[OK] {label}: extract succeeded, {} styles",
16820+ output.styles.len()
16821+ );
16822+ }
16823+ Ok(Err(e)) => {
16824+ panic!("[FAIL] {label}: extract returned Err: {e}");
16825+ }
16826+ Err(panic_payload) => {
16827+ let msg = panic_payload
16828+ .downcast_ref::<&'static str>()
16829+ .map(|s| (*s).to_string())
16830+ .or_else(|| panic_payload.downcast_ref::<String>().cloned())
16831+ .unwrap_or_else(|| "<non-string panic>".to_string());
16832+ panic!("[FAIL] {label}: extract panicked: {msg}");
16833+ }
16834+ }
16835+ }
16836+ }
16837+
16838+ #[test]
16839+ #[serial]
16840+ fn extract_pseudo_selector_with_identifier_snapshot() {
16841+ // Snapshot-locks the current behavior for the minimal ERROR.tsx
16842+ // reduction so future refactors can't silently regress either
16843+ // direction (re-introducing the panic, or over-eagerly turning the
16844+ // `_hover={ident}` attribute into something surprising).
16845+ reset_class_map();
16846+ reset_file_map();
16847+ assert_debug_snapshot!(ToBTreeSet::from(
16848+ extract(
16849+ "test.tsx",
16850+ r#"import {Box} from '@devup-ui/react'
16851+ const hoverStyle = { opacity: 1 };
16852+ export const A = () => <Box _hover={hoverStyle} bg="red" />;
16853+ "#,
16854+ ExtractOption {
16855+ package: "@devup-ui/react".to_string(),
16856+ css_dir: "@devup-ui/react".to_string(),
16857+ single_css: true,
16858+ import_main_css: false,
16859+ import_aliases: HashMap::new(),
16860+ },
16861+ )
16862+ .unwrap()
16863+ ));
16864+ }
1672716865}
0 commit comments