@barefootjs/rust 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -0
- package/dist/adapter/analysis/component-tree.d.ts +26 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +85 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +107 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +143 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189091 -0
- package/dist/adapter/lib/constants.d.ts +25 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +50 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/minijinja-naming.d.ts +64 -0
- package/dist/adapter/lib/minijinja-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +32 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +84 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/minijinja-adapter.d.ts +421 -0
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +33 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +63 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +28 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +29 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189111 -0
- package/dist/conformance-pins.d.ts +13 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189112 -0
- package/package.json +67 -0
- package/runtime/Cargo.lock +124 -0
- package/runtime/Cargo.toml +21 -0
- package/runtime/src/backend_minijinja.rs +176 -0
- package/runtime/src/bin/bf-render.rs +147 -0
- package/runtime/src/evaluator.rs +770 -0
- package/runtime/src/lib.rs +19 -0
- package/runtime/src/manifest.rs +258 -0
- package/runtime/src/num.rs +558 -0
- package/runtime/src/runtime.rs +1548 -0
- package/runtime/src/search_params.rs +173 -0
- package/runtime/tests/eval_vectors.rs +94 -0
- package/runtime/tests/evaluator.rs +407 -0
- package/runtime/tests/helper_vectors.rs +348 -0
- package/runtime/tests/manifest.rs +169 -0
- package/runtime/tests/omit.rs +79 -0
- package/runtime/tests/props_attr.rs +75 -0
- package/runtime/tests/query.rs +50 -0
- package/runtime/tests/render_child.rs +210 -0
- package/runtime/tests/search_params.rs +68 -0
- package/runtime/tests/spread_attrs.rs +94 -0
- package/runtime/tests/template_primitives.rs +376 -0
- package/runtime/tests/vector-divergences.json +33 -0
- package/src/__tests__/minijinja-adapter-unit.test.ts +392 -0
- package/src/__tests__/minijinja-adapter.test.ts +58 -0
- package/src/__tests__/minijinja-counter.test.ts +61 -0
- package/src/__tests__/minijinja-query-href.test.ts +101 -0
- package/src/__tests__/minijinja-spread-attrs.test.ts +227 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +177 -0
- package/src/adapter/emit-context.ts +119 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/lib/constants.ts +37 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/minijinja-naming.ts +85 -0
- package/src/adapter/lib/types.ts +35 -0
- package/src/adapter/memo/seed.ts +135 -0
- package/src/adapter/minijinja-adapter.ts +1796 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +168 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +38 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +12 -0
- package/src/test-render.ts +680 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//! `runtime::query` -- ported from
|
|
2
|
+
//! `packages/adapter-jinja/python/tests/test_query.py` (itself a port of
|
|
3
|
+
//! `packages/adapter-perl/t/query.t`).
|
|
4
|
+
//!
|
|
5
|
+
//! The full CROSS-BACKEND behaviour (control flow + form-encoding parity
|
|
6
|
+
//! with the browser's `URLSearchParams`) is defined ONCE in the shared
|
|
7
|
+
//! golden helper vectors and run by `tests/helper_vectors.rs`. This file
|
|
8
|
+
//! keeps a few representative cases for always-on coverage plus a
|
|
9
|
+
//! Rust-runtime-specific defensive case the golden vectors can't express: a
|
|
10
|
+
//! `null` value (JSON has no `undefined`; a JSON `null` stringifies to
|
|
11
|
+
//! `"null"` under JS `String()`, so it can't be a shared vector -- this
|
|
12
|
+
//! runtime coerces `null` to `''` and omits the empty pair, mirroring the
|
|
13
|
+
//! Perl/Python ports' documented `undef`/`None` handling).
|
|
14
|
+
|
|
15
|
+
use barefootjs::num::JsValue;
|
|
16
|
+
use barefootjs::runtime;
|
|
17
|
+
|
|
18
|
+
fn s(v: &str) -> JsValue {
|
|
19
|
+
JsValue::from(v)
|
|
20
|
+
}
|
|
21
|
+
fn b(v: bool) -> JsValue {
|
|
22
|
+
JsValue::Bool(v)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[test]
|
|
26
|
+
fn order_preserved_repeated_key_overwrites_at_first_position() {
|
|
27
|
+
let triples = [b(true), s("sort"), s("title"), b(true), s("tag"), s("go"), b(true), s("sort"), s("date")];
|
|
28
|
+
assert_eq!(runtime::query(&s("/blog"), &triples), "/blog?sort=date&tag=go");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn form_encoding_tilde_star_space() {
|
|
33
|
+
let triples = [b(true), s("t"), s("a~b *c")];
|
|
34
|
+
assert_eq!(runtime::query(&s("/s"), &triples), "/s?t=a%7Eb+*c");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn array_value_appends_pair_per_nonempty_member() {
|
|
39
|
+
let triples = [b(true), s("tag"), JsValue::Array(vec![s("a"), s(""), s("b")])];
|
|
40
|
+
assert_eq!(runtime::query(&s("/list"), &triples), "/list?tag=a&tag=b");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn none_value_coerced_to_empty_and_omitted() {
|
|
45
|
+
let triples = [b(true), s("tag"), JsValue::Null];
|
|
46
|
+
assert_eq!(runtime::query(&s("/list"), &triples), "/list");
|
|
47
|
+
|
|
48
|
+
let triples2 = [b(true), s("tag"), JsValue::Null, b(true), s("keep"), s("me")];
|
|
49
|
+
assert_eq!(runtime::query(&s("/list"), &triples2), "/list?keep=me");
|
|
50
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
//! `render_child` renderer-invocation contract, ported from
|
|
2
|
+
//! `packages/adapter-jinja/python/tests/test_render_child.py` (itself a
|
|
3
|
+
//! port of `packages/adapter-perl/t/render_child.t`).
|
|
4
|
+
//!
|
|
5
|
+
//! Renderer contract (#1897): a child's scope/slot identity chains off the
|
|
6
|
+
//! CALLER's `bf` instance (`self` inside `Object::call_method`), not off
|
|
7
|
+
//! whichever instance originally registered the renderer. Unlike the
|
|
8
|
+
//! Python port (which can stub a bare `BarefootJS` instance and call
|
|
9
|
+
//! `render_child` directly with a hand-built backend), this crate's
|
|
10
|
+
//! `render_child` takes a live `&minijinja::State` -- there is no public
|
|
11
|
+
//! way to construct one outside an active render -- so these tests drive
|
|
12
|
+
//! the SAME contract through real `.j2` template renders (mirroring what
|
|
13
|
+
//! `bf-render` does), which is the natural Rust-shaped equivalent and
|
|
14
|
+
//! additionally exercises the actual `Object::call_method` dispatch path.
|
|
15
|
+
|
|
16
|
+
use barefootjs::num::JsValue;
|
|
17
|
+
use barefootjs::runtime::{js_to_mj, ChildRendererSpec};
|
|
18
|
+
use barefootjs::{backend_minijinja, BfInstance, RenderSession};
|
|
19
|
+
use minijinja::value::Value as MjValue;
|
|
20
|
+
use std::collections::BTreeMap;
|
|
21
|
+
use std::path::PathBuf;
|
|
22
|
+
use std::sync::Arc;
|
|
23
|
+
|
|
24
|
+
struct TempDir(PathBuf);
|
|
25
|
+
|
|
26
|
+
impl TempDir {
|
|
27
|
+
fn new(tag: &str) -> TempDir {
|
|
28
|
+
let dir = std::env::temp_dir().join(format!("bf-render-child-test-{tag}-{}-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
|
|
29
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
30
|
+
TempDir(dir)
|
|
31
|
+
}
|
|
32
|
+
fn write(&self, name: &str, content: &str) {
|
|
33
|
+
std::fs::write(self.0.join(name), content).unwrap();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl Drop for TempDir {
|
|
38
|
+
fn drop(&mut self) {
|
|
39
|
+
std::fs::remove_dir_all(&self.0).ok();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// `vars` for [`backend_minijinja::render_entry`] (top-level render), which
|
|
44
|
+
/// still takes JSON-shaped `&JsValue`.
|
|
45
|
+
fn no_defaults() -> JsValue {
|
|
46
|
+
JsValue::Object(BTreeMap::new())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// `ChildRendererSpec::ssr_defaults`, which is `minijinja::Value` (props
|
|
50
|
+
/// stay `Value` end-to-end through `render_child` -- see its docstring).
|
|
51
|
+
fn no_ssr_defaults() -> MjValue {
|
|
52
|
+
MjValue::from(BTreeMap::<String, MjValue>::new())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn nested_render_child_chains_scope_off_the_caller_not_the_registrant() {
|
|
57
|
+
let dir = TempDir::new("nested");
|
|
58
|
+
dir.write("root.j2", "{{ bf.render_child('mid', {'_bf_slot': 's0'}) | safe }}");
|
|
59
|
+
dir.write("mid.j2", "[{{ bf.scope_attr() }}:{{ bf.render_child('leaf', {'_bf_slot': 'inner'}) | safe }}]");
|
|
60
|
+
dir.write("leaf.j2", "leaf@{{ bf.scope_attr() }}");
|
|
61
|
+
|
|
62
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
63
|
+
let session = RenderSession::new();
|
|
64
|
+
session.register_child_renderer(
|
|
65
|
+
"mid".to_string(),
|
|
66
|
+
ChildRendererSpec { component_name: "Mid".to_string(), template: "mid".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
|
|
67
|
+
);
|
|
68
|
+
session.register_child_renderer(
|
|
69
|
+
"leaf".to_string(),
|
|
70
|
+
ChildRendererSpec { component_name: "Leaf".to_string(), template: "leaf".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
let root = BfInstance::root(Arc::clone(&session), "Root_test");
|
|
74
|
+
let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
|
|
75
|
+
|
|
76
|
+
// `mid`'s scope is `<root scope>_s0`; `leaf` is rendered from INSIDE
|
|
77
|
+
// `mid`'s template (where `bf` is the `mid` instance), so its scope
|
|
78
|
+
// chains off `mid`'s scope (`Root_test_s0_inner`), NOT off the root's
|
|
79
|
+
// scope directly (`Root_test_inner`) -- proving the caller-chaining
|
|
80
|
+
// contract.
|
|
81
|
+
assert_eq!(out, "[Root_test_s0:leaf@Root_test_s0_inner]");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[test]
|
|
85
|
+
fn missing_renderer_propagates_as_render_error() {
|
|
86
|
+
let dir = TempDir::new("missing");
|
|
87
|
+
dir.write("root.j2", "{{ bf.render_child('missing') }}");
|
|
88
|
+
|
|
89
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
90
|
+
let session = RenderSession::new();
|
|
91
|
+
let root = BfInstance::root(session, "Root_test");
|
|
92
|
+
let err = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap_err();
|
|
93
|
+
assert!(err.to_string().contains("No renderer registered for child component 'missing'"), "unexpected error: {err}");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn reserved_word_prop_is_mangled() {
|
|
98
|
+
let dir = TempDir::new("reserved");
|
|
99
|
+
dir.write("root.j2", "{{ bf.render_child('kw', {'class': 'x', 'id': 'y'}) }}");
|
|
100
|
+
dir.write("kw.j2", "{{ class_ }}-{{ id }}");
|
|
101
|
+
|
|
102
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
103
|
+
let session = RenderSession::new();
|
|
104
|
+
session.register_child_renderer(
|
|
105
|
+
"kw".to_string(),
|
|
106
|
+
ChildRendererSpec { component_name: "Kw".to_string(), template: "kw".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
|
|
107
|
+
);
|
|
108
|
+
let root = BfInstance::root(session, "Root_test");
|
|
109
|
+
let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
|
|
110
|
+
assert_eq!(out, "x-y");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn rest_bag_routing_and_ssr_defaults_merge() {
|
|
115
|
+
let dir = TempDir::new("restbag");
|
|
116
|
+
// `Card` declares `title` as a named param and destructures the rest
|
|
117
|
+
// into `rest`; `subtitle` isn't a declared param, so it must land in
|
|
118
|
+
// `rest`, not as a top-level stash var.
|
|
119
|
+
dir.write("root.j2", "{{ bf.render_child('card', {'title': 'Hi', 'subtitle': 'Sub', 'extra': 'E'}) }}");
|
|
120
|
+
dir.write("card.j2", "{{ title }}|{{ rest.subtitle }}|{{ rest.extra }}|{{ theme }}");
|
|
121
|
+
|
|
122
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
123
|
+
let session = RenderSession::new();
|
|
124
|
+
let mut defaults = BTreeMap::new();
|
|
125
|
+
defaults.insert("theme".to_string(), JsValue::from("dark"));
|
|
126
|
+
session.register_child_renderer(
|
|
127
|
+
"card".to_string(),
|
|
128
|
+
ChildRendererSpec {
|
|
129
|
+
component_name: "Card".to_string(),
|
|
130
|
+
template: "card".to_string(),
|
|
131
|
+
ssr_defaults: js_to_mj(&JsValue::Object(defaults)),
|
|
132
|
+
rest_props_name: Some("rest".to_string()),
|
|
133
|
+
param_names: vec!["title".to_string()],
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
let root = BfInstance::root(session, "Root_test");
|
|
137
|
+
let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
|
|
138
|
+
assert_eq!(out, "Hi|Sub|E|dark");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn loop_child_without_slot_gets_a_fresh_component_prefixed_scope() {
|
|
143
|
+
let dir = TempDir::new("loopchild");
|
|
144
|
+
dir.write("root.j2", "{{ bf.render_child('item', {}) }}|{{ bf.render_child('item', {}) }}");
|
|
145
|
+
dir.write("item.j2", "{{ bf.scope_attr() }}");
|
|
146
|
+
|
|
147
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
148
|
+
let session = RenderSession::new();
|
|
149
|
+
session.register_child_renderer(
|
|
150
|
+
"item".to_string(),
|
|
151
|
+
ChildRendererSpec { component_name: "Item".to_string(), template: "item".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
|
|
152
|
+
);
|
|
153
|
+
let root = BfInstance::root(session, "Root_test");
|
|
154
|
+
let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
|
|
155
|
+
let parts: Vec<&str> = out.split('|').collect();
|
|
156
|
+
assert_eq!(parts.len(), 2);
|
|
157
|
+
for p in &parts {
|
|
158
|
+
assert!(p.starts_with("Item_"), "expected an Item_<rand> scope id, got {p:?}");
|
|
159
|
+
}
|
|
160
|
+
assert_ne!(parts[0], parts[1], "two loop-child renders must get distinct scope ids");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[test]
|
|
164
|
+
fn childless_invocation_renders_with_no_props() {
|
|
165
|
+
let dir = TempDir::new("childless");
|
|
166
|
+
dir.write("root.j2", "{{ bf.render_child('leaf') }}");
|
|
167
|
+
dir.write("leaf.j2", "leaf-ok");
|
|
168
|
+
|
|
169
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
170
|
+
let session = RenderSession::new();
|
|
171
|
+
session.register_child_renderer(
|
|
172
|
+
"leaf".to_string(),
|
|
173
|
+
ChildRendererSpec { component_name: "Leaf".to_string(), template: "leaf".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
|
|
174
|
+
);
|
|
175
|
+
let root = BfInstance::root(session, "Root_test");
|
|
176
|
+
let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
|
|
177
|
+
assert_eq!(out, "leaf-ok");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn children_safe_value_survives_render_child_and_bf_string_unescaped() {
|
|
182
|
+
// Regression for the "children lost their safe flag" bug: a JSX
|
|
183
|
+
// children capture (`{% set cap %}...{% endset %}`, mirroring what the
|
|
184
|
+
// TS emitter generates for children forwarding) is a SAFE `Value`.
|
|
185
|
+
// Passed as a child prop through `bf.string(...)` (as the emitter does
|
|
186
|
+
// at every text/attribute-position value) and through
|
|
187
|
+
// `bf.render_child`'s prop plumbing, it must reach the child template
|
|
188
|
+
// STILL safe -- i.e. NOT HTML-escaped -- proving both fixes: (1)
|
|
189
|
+
// `render_child` keeps props as `Value` end-to-end instead of
|
|
190
|
+
// round-tripping through `JsValue` (which has no safe/unsafe
|
|
191
|
+
// distinction), and (2) `bf.string` passes an already-safe string
|
|
192
|
+
// input through unchanged (mirrors the Python runtime's `js_string`,
|
|
193
|
+
// where a `str` input -- including a `Markup` -- is returned as-is).
|
|
194
|
+
let dir = TempDir::new("safechildren");
|
|
195
|
+
dir.write(
|
|
196
|
+
"root.j2",
|
|
197
|
+
"{% set cap %}<span>hi</span>{% endset %}{{ bf.render_child('wrap', {'inner': bf.string(cap)}) | safe }}",
|
|
198
|
+
);
|
|
199
|
+
dir.write("wrap.j2", "[{{ inner }}]");
|
|
200
|
+
|
|
201
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
202
|
+
let session = RenderSession::new();
|
|
203
|
+
session.register_child_renderer(
|
|
204
|
+
"wrap".to_string(),
|
|
205
|
+
ChildRendererSpec { component_name: "Wrap".to_string(), template: "wrap".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
|
|
206
|
+
);
|
|
207
|
+
let root = BfInstance::root(session, "Root_test");
|
|
208
|
+
let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
|
|
209
|
+
assert_eq!(out, "[<span>hi</span>]");
|
|
210
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//! `barefootjs::SearchParams` -- Rust-specific concerns, ported from
|
|
2
|
+
//! `packages/adapter-jinja/python/tests/test_search_params.py` (itself a
|
|
3
|
+
//! port of `packages/adapter-perl/t/search_params.t`).
|
|
4
|
+
//!
|
|
5
|
+
//! The cross-language VALUE semantics of `get` are owned by the
|
|
6
|
+
//! language-independent golden vectors (`search_params_get` in
|
|
7
|
+
//! `tests/helper_vectors.rs`), so Go/Perl/Python/Rust parity there is
|
|
8
|
+
//! mechanical. This file covers only what those value vectors can't: the
|
|
9
|
+
//! constructor seam, lenient parsing (never panics), and UTF-8 decoding.
|
|
10
|
+
//! (Mirrors `src/search_params.rs`'s inline `#[cfg(test)]` module -- kept
|
|
11
|
+
//! as its own top-level file too, per the design doc's file layout, since
|
|
12
|
+
//! it is the direct structural port of the Python test module.)
|
|
13
|
+
|
|
14
|
+
use barefootjs::SearchParams;
|
|
15
|
+
|
|
16
|
+
#[test]
|
|
17
|
+
fn constructor_and_get() {
|
|
18
|
+
let sp = SearchParams::new("sort=price");
|
|
19
|
+
assert_eq!(sp.get("sort"), Some("price"));
|
|
20
|
+
assert_eq!(SearchParams::new("").get("sort"), None);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[test]
|
|
24
|
+
fn none_composition_coalesces_only_none() {
|
|
25
|
+
// The adapters lower `searchParams().get(k) ?? d` to a minijinja
|
|
26
|
+
// expression that coalesces only an absent key (not a bare `or`,
|
|
27
|
+
// which would also default a present-but-empty value) -- so an
|
|
28
|
+
// absent key falls back to the author's default while a
|
|
29
|
+
// present-but-empty value keeps ''.
|
|
30
|
+
let absent = SearchParams::new("other=x");
|
|
31
|
+
assert_eq!(absent.get("sort").unwrap_or("none"), "none");
|
|
32
|
+
|
|
33
|
+
let empty = SearchParams::new("sort=");
|
|
34
|
+
assert_eq!(empty.get("sort").unwrap_or("none"), "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn utf8_percent_decoding() {
|
|
39
|
+
let sp = SearchParams::new("q=%E2%9C%93");
|
|
40
|
+
assert_eq!(sp.get("q"), Some("\u{2713}"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn lenient_parsing_never_panics() {
|
|
45
|
+
let _ = SearchParams::new("");
|
|
46
|
+
assert_eq!(SearchParams::new("&&&").get("x"), None);
|
|
47
|
+
assert_eq!(SearchParams::new("=novalue").get("x"), None);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn to_value_exposes_get_method_to_templates() {
|
|
52
|
+
// Smoke-test the `minijinja::value::Object` wiring end-to-end via a
|
|
53
|
+
// real template render (`bf-render`'s own path), rather than only unit
|
|
54
|
+
// testing `SearchParams::get` directly.
|
|
55
|
+
let dir = std::env::temp_dir().join(format!("bf-search-params-test-{}", std::process::id()));
|
|
56
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
57
|
+
std::fs::write(dir.join("t.j2"), "{{ searchParams.get('sort') }}|{{ (searchParams.get('missing') if (searchParams.get('missing') is defined and searchParams.get('missing') is not none) else 'default') }}").unwrap();
|
|
58
|
+
|
|
59
|
+
let env = barefootjs::backend_minijinja::build_environment(&dir);
|
|
60
|
+
let session = barefootjs::RenderSession::new();
|
|
61
|
+
let root = barefootjs::BfInstance::root(session, "test");
|
|
62
|
+
let vars = barefootjs::num::JsValue::Object(Default::default());
|
|
63
|
+
let extra = vec![("searchParams".to_string(), SearchParams::new("sort=price").to_value())];
|
|
64
|
+
let out = barefootjs::backend_minijinja::render_entry(&env, "t", root.as_mj_value(), &vars, &extra).unwrap();
|
|
65
|
+
assert_eq!(out, "price|default");
|
|
66
|
+
|
|
67
|
+
std::fs::remove_dir_all(&dir).ok();
|
|
68
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//! `runtime::spread_attrs` -- ported from
|
|
2
|
+
//! `packages/adapter-jinja/python/tests/test_spread_attrs.py` (itself a
|
|
3
|
+
//! port of `packages/adapter-perl/t/spread_attrs.t`).
|
|
4
|
+
//!
|
|
5
|
+
//! JSX intrinsic-element spread runtime helper (#1407 follow-up). Mirrors
|
|
6
|
+
//! the JS `spreadAttrs` runtime and the Go/Perl/Python adapters'
|
|
7
|
+
//! equivalents so SSR output stays byte-equal across every adapter --
|
|
8
|
+
//! cross-adapter parity regressions surface here first.
|
|
9
|
+
|
|
10
|
+
use barefootjs::num::JsValue;
|
|
11
|
+
use barefootjs::runtime;
|
|
12
|
+
use std::collections::BTreeMap;
|
|
13
|
+
|
|
14
|
+
fn obj(pairs: &[(&str, JsValue)]) -> JsValue {
|
|
15
|
+
JsValue::Object(pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect::<BTreeMap<_, _>>())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn s(v: &str) -> JsValue {
|
|
19
|
+
JsValue::from(v)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fn run(bag: &JsValue) -> String {
|
|
23
|
+
runtime::spread_attrs(bag)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn basic_shapes() {
|
|
28
|
+
assert_eq!(run(&JsValue::Null), "");
|
|
29
|
+
assert_eq!(run(&obj(&[])), "");
|
|
30
|
+
assert_eq!(run(&s("not a hash")), "");
|
|
31
|
+
assert_eq!(run(&obj(&[("id", s("a"))])), "id=\"a\"");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn alphabetic_key_order() {
|
|
36
|
+
assert_eq!(run(&obj(&[("id", s("a")), ("class", s("on"))])), "class=\"on\" id=\"a\"");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[test]
|
|
40
|
+
fn key_remapping() {
|
|
41
|
+
assert_eq!(run(&obj(&[("className", s("foo"))])), "class=\"foo\"");
|
|
42
|
+
assert_eq!(run(&obj(&[("htmlFor", s("x"))])), "for=\"x\"");
|
|
43
|
+
assert_eq!(run(&obj(&[("dataPriority", s("high"))])), "data-priority=\"high\"");
|
|
44
|
+
// SVG XML attrs are case-sensitive -- preserve verbatim.
|
|
45
|
+
assert_eq!(run(&obj(&[("viewBox", s("0 0 10 10"))])), "viewBox=\"0 0 10 10\"");
|
|
46
|
+
assert_eq!(run(&obj(&[("clipPathUnits", s("userSpaceOnUse"))])), "clipPathUnits=\"userSpaceOnUse\"");
|
|
47
|
+
// JS-reference parity (#1411): a leading uppercase letter emits a
|
|
48
|
+
// leading dash.
|
|
49
|
+
assert_eq!(run(&obj(&[("XData", s("x"))])), "-x-data=\"x\"");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn event_handlers_js_predicate_parity() {
|
|
54
|
+
assert_eq!(run(&obj(&[("onClick", s("fn")), ("id", s("a"))])), "id=\"a\"");
|
|
55
|
+
assert_eq!(run(&obj(&[("on_custom", s("fn")), ("id", s("a"))])), "id=\"a\"");
|
|
56
|
+
assert_eq!(run(&obj(&[("on0", s("fn")), ("id", s("a"))])), "id=\"a\"");
|
|
57
|
+
assert_eq!(run(&obj(&[("oncology", s("x"))])), "oncology=\"x\"");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn children_skipped_ref_passed_through() {
|
|
62
|
+
assert_eq!(run(&obj(&[("children", s("x")), ("id", s("a"))])), "id=\"a\"");
|
|
63
|
+
// JS `spreadAttrs` does NOT filter `ref` (`applyRestAttrs` does --
|
|
64
|
+
// that's a separate divergence).
|
|
65
|
+
assert_eq!(run(&obj(&[("ref", s("x")), ("id", s("a"))])), "id=\"a\" ref=\"x\"");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[test]
|
|
69
|
+
fn boolean_values() {
|
|
70
|
+
assert_eq!(run(&obj(&[("hidden", JsValue::Bool(true)), ("id", s("a"))])), "hidden id=\"a\"");
|
|
71
|
+
assert_eq!(run(&obj(&[("hidden", JsValue::Bool(false)), ("id", s("a"))])), "id=\"a\"");
|
|
72
|
+
// Plain numeric 0 renders as a value (matches `tabindex="0"`).
|
|
73
|
+
assert_eq!(run(&obj(&[("tabindex", JsValue::Number(0.0))])), "tabindex=\"0\"");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn nullish_skip() {
|
|
78
|
+
assert_eq!(run(&obj(&[("a", JsValue::Null), ("b", s("x"))])), "b=\"x\"");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn html_escape() {
|
|
83
|
+
assert_eq!(run(&obj(&[("title", s("<b>\"x\"</b>"))])), "title=\"<b>"x"</b>\"");
|
|
84
|
+
assert_eq!(run(&obj(&[("alt", s("tom & jerry"))])), "alt=\"tom & jerry\"");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn style_object_lowering() {
|
|
89
|
+
assert_eq!(
|
|
90
|
+
run(&obj(&[("style", obj(&[("backgroundColor", s("red")), ("color", s("white"))]))])),
|
|
91
|
+
"style=\"background-color:red;color:white\""
|
|
92
|
+
);
|
|
93
|
+
assert_eq!(run(&obj(&[("style", s("color:red"))])), "style=\"color:red\"");
|
|
94
|
+
}
|