|
| 1 | +use rstest::rstest; |
| 2 | +use serial_test::serial; |
| 3 | + |
| 4 | +use super::extracted; |
| 5 | + |
| 6 | +#[rstest] |
| 7 | +#[case("{ [require('@devup-ui/react/compat/stylex')]: value }", "require")] |
| 8 | +#[case("{ [import('@devup-ui/react/compat/stylex')]: value }", "import(...)")] |
| 9 | +#[case("{ getTheme = require('@devup-ui/react/compat/stylex') }", "require")] |
| 10 | +#[case( |
| 11 | + "{ getTheme: { missing = require('@devup-ui/react/compat/stylex') } }", |
| 12 | + "require" |
| 13 | +)] |
| 14 | +#[case("[value = require('@devup-ui/react/compat/stylex')]", "require")] |
| 15 | +#[case( |
| 16 | + "{ getTheme: [value = import('@devup-ui/react/compat/stylex')] }", |
| 17 | + "import(...)" |
| 18 | +)] |
| 19 | +#[case( |
| 20 | + "[...{ [require('@devup-ui/react/compat/stylex')]: value }]", |
| 21 | + "require" |
| 22 | +)] |
| 23 | +#[serial] |
| 24 | +fn types_only_loader_errors_when_executed_in_an_accepted_binding_pattern( |
| 25 | + #[case] pattern: &str, |
| 26 | + #[case] loader: &str, |
| 27 | +) { |
| 28 | + // Given |
| 29 | + let declaration = format!("const {pattern} = require('@devup-ui/react');"); |
| 30 | + let code = format!("const before = 0;\n{declaration}"); |
| 31 | + let column = declaration |
| 32 | + .find("require('@devup-ui/react/compat/stylex')") |
| 33 | + .or_else(|| declaration.find("import('@devup-ui/react/compat/stylex')")) |
| 34 | + .unwrap_or_else(|| panic!("fixture must contain the inner loader")) |
| 35 | + + 1; |
| 36 | + // When |
| 37 | + let error = match extracted(&code) { |
| 38 | + Err(error) => error, |
| 39 | + Ok(output) => panic!("binding-pattern loader must fail: {output}"), |
| 40 | + }; |
| 41 | + // Then |
| 42 | + assert_eq!( |
| 43 | + error, |
| 44 | + format!( |
| 45 | + "test.tsx:2:{column}: `StyleX API()` cannot use `{loader}` at build time: compat/stylex has only type exports; use the real stylex entrypoint" |
| 46 | + ) |
| 47 | + ); |
| 48 | +} |
| 49 | + |
| 50 | +#[rstest] |
| 51 | +#[case( |
| 52 | + "{ [pt.call(null, { top: 1 })]: value }", |
| 53 | + "pt.call", |
| 54 | + "invocation/binding member" |
| 55 | +)] |
| 56 | +#[case( |
| 57 | + "{ getTheme = pt.apply(null, [{ top: 1 }]) }", |
| 58 | + "pt.apply", |
| 59 | + "invocation/binding member" |
| 60 | +)] |
| 61 | +#[case("{ getTheme = pt.bind(null) }", "pt.bind", "invocation/binding member")] |
| 62 | +#[case("{ getTheme = consume(pt) }", "pt", "argument passing")] |
| 63 | +#[case("{ getTheme = { api: pt } }", "pt", "object storage")] |
| 64 | +#[case("{ getTheme: [value = [pt]] }", "pt", "array storage")] |
| 65 | +#[case("[...{ [consume(pt)]: value }]", "pt", "argument passing")] |
| 66 | +#[serial] |
| 67 | +fn known_api_escape_errors_when_executed_in_an_accepted_binding_pattern( |
| 68 | + #[case] pattern: &str, |
| 69 | + #[case] site: &str, |
| 70 | + #[case] form: &str, |
| 71 | +) { |
| 72 | + // Given |
| 73 | + let declaration = format!("const {pattern} = require('@devup-ui/react');"); |
| 74 | + let code = format!("import {{ positionTry as pt }} from '@stylexjs/stylex';\n{declaration}"); |
| 75 | + let column = declaration |
| 76 | + .find(site) |
| 77 | + .unwrap_or_else(|| panic!("fixture must contain the API occurrence")) |
| 78 | + + 1; |
| 79 | + // When |
| 80 | + let error = match extracted(&code) { |
| 81 | + Err(error) => error, |
| 82 | + Ok(output) => panic!("binding-pattern API escape must fail: {output}"), |
| 83 | + }; |
| 84 | + // Then |
| 85 | + assert_eq!( |
| 86 | + error, |
| 87 | + format!( |
| 88 | + "test.tsx:2:{column}: `stylex.positionTry()` cannot use `{site}` at build time: unsupported {form}; call the API directly" |
| 89 | + ) |
| 90 | + ); |
| 91 | +} |
| 92 | + |
| 93 | +#[rstest] |
| 94 | +#[case("{ [key()]: value }", "[key()]: value")] |
| 95 | +#[case("{ getTheme = fallback }", "getTheme = fallback")] |
| 96 | +#[case("{ getTheme: { missing = fallback } }", "missing = fallback")] |
| 97 | +#[case("[value = fallback]", "[value = fallback]")] |
| 98 | +#[case("[...{ [key()]: value }]", "[key()]: value")] |
| 99 | +#[case("{ getTheme, ...rest }", "...rest")] |
| 100 | +#[serial] |
| 101 | +fn ordinary_binding_patterns_remain_when_the_stylex_gate_is_active( |
| 102 | + #[case] pattern: &str, |
| 103 | + #[case] retained: &str, |
| 104 | +) { |
| 105 | + // Given |
| 106 | + let code = format!( |
| 107 | + "import {{ positionTry as pt }} from '@stylexjs/stylex';\nconst {pattern} = require('@devup-ui/react'); const name = pt({{ top: '1px' }});" |
| 108 | + ); |
| 109 | + // When |
| 110 | + let output = extracted(&code) |
| 111 | + .unwrap_or_else(|error| panic!("ordinary binding pattern must compile: {error}")); |
| 112 | + // Then |
| 113 | + assert!(output.contains(retained), "{output}"); |
| 114 | + assert!(output.contains("require(\"@devup-ui/react\")"), "{output}"); |
| 115 | + assert!(!output.contains("pt("), "{output}"); |
| 116 | +} |
| 117 | + |
| 118 | +#[test] |
| 119 | +#[serial] |
| 120 | +fn mixed_root_pattern_keeps_its_runtime_default_when_stylex_is_consumed() { |
| 121 | + // Given |
| 122 | + let code = "const { stylex: sx, getTheme = fallback } = require('@devup-ui/react'); const name = sx.positionTry({ top: '1px' });"; |
| 123 | + // When |
| 124 | + let output = |
| 125 | + extracted(code).unwrap_or_else(|error| panic!("mixed root pattern must compile: {error}")); |
| 126 | + // Then |
| 127 | + assert!(output.contains("getTheme = fallback"), "{output}"); |
| 128 | + assert!(output.contains("require(\"@devup-ui/react\")"), "{output}"); |
| 129 | + assert!(!output.contains("stylex: sx"), "{output}"); |
| 130 | + assert!(!output.contains("positionTry("), "{output}"); |
| 131 | +} |
| 132 | + |
| 133 | +#[rstest] |
| 134 | +#[case( |
| 135 | + "const sx = require('@devup-ui/react/stylex'); const name = sx.positionTry({ top: '1px' });" |
| 136 | +)] |
| 137 | +#[case("const { positionTry: pt } = require('@stylexjs/stylex'); const name = pt({ top: '1px' });")] |
| 138 | +#[case( |
| 139 | + "const root = require('@devup-ui/react'); const name = root.stylex.positionTry({ top: '1px' });" |
| 140 | +)] |
| 141 | +#[serial] |
| 142 | +fn supported_outer_loader_binding_identifiers_are_not_api_reads(#[case] code: &str) { |
| 143 | + // Given: a supported direct loader and a consumed API call. |
| 144 | + // When |
| 145 | + let output = extracted(code) |
| 146 | + .unwrap_or_else(|error| panic!("supported outer loader must compile: {error}")); |
| 147 | + // Then |
| 148 | + assert!(output.contains("const name = \""), "{output}"); |
| 149 | + assert!(!output.contains("positionTry("), "{output}"); |
| 150 | + assert!(!output.contains("require("), "{output}"); |
| 151 | +} |
| 152 | + |
| 153 | +#[rstest] |
| 154 | +#[case( |
| 155 | + "{ getTheme = ((require) => require('@devup-ui/react/compat/stylex'))(loader) }", |
| 156 | + "require(\"@devup-ui/react/compat/stylex\")" |
| 157 | +)] |
| 158 | +#[case("{ [((pt) => pt.call(null))(fallback)]: value }", "pt.call(null)")] |
| 159 | +#[case("{ getTheme = ((pt) => pt.bind(null))(fallback) }", "pt.bind(null)")] |
| 160 | +#[serial] |
| 161 | +fn shadowed_pattern_loaders_and_functions_remain_when_the_gate_is_active( |
| 162 | + #[case] pattern: &str, |
| 163 | + #[case] retained: &str, |
| 164 | +) { |
| 165 | + // Given |
| 166 | + let code = format!( |
| 167 | + "import {{ positionTry as pt }} from '@stylexjs/stylex';\nconst {pattern} = require('@devup-ui/react'); const name = pt({{ top: '1px' }});" |
| 168 | + ); |
| 169 | + // When |
| 170 | + let output = extracted(&code) |
| 171 | + .unwrap_or_else(|error| panic!("shadowed pattern expression must compile: {error}")); |
| 172 | + // Then |
| 173 | + assert!(output.contains(retained), "{output}"); |
| 174 | + assert!(output.contains("require(\"@devup-ui/react\")"), "{output}"); |
| 175 | + assert!(output.contains("const name = \""), "{output}"); |
| 176 | +} |
| 177 | + |
| 178 | +#[test] |
| 179 | +#[serial] |
| 180 | +fn erased_pattern_types_are_skipped_when_the_gate_is_active() { |
| 181 | + // Given |
| 182 | + let code = "import { positionTry as pt } from '@stylexjs/stylex'; |
| 183 | +const { getTheme = fallback }: { getTheme: typeof pt; styles?: import('@devup-ui/react/compat/stylex').PositionTryStyles } = require('@devup-ui/react'); |
| 184 | +const name = pt({ top: '1px' });"; |
| 185 | + // When |
| 186 | + let output = extracted(code) |
| 187 | + .unwrap_or_else(|error| panic!("erased binding types must compile: {error}")); |
| 188 | + // Then |
| 189 | + assert!(output.contains("getTheme = fallback"), "{output}"); |
| 190 | + assert!(output.contains("require(\"@devup-ui/react\")"), "{output}"); |
| 191 | + assert!(output.contains("const name = \""), "{output}"); |
| 192 | +} |
0 commit comments