@aihu/css-engine 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/LICENSE +21 -0
- package/README.md +122 -0
- package/crates/aihu-css-core/Cargo.toml +22 -0
- package/crates/aihu-css-core/src/ast.rs +173 -0
- package/crates/aihu-css-core/src/bin/main.rs +73 -0
- package/crates/aihu-css-core/src/cache.rs +182 -0
- package/crates/aihu-css-core/src/emit.rs +236 -0
- package/crates/aihu-css-core/src/features/anchor.rs +41 -0
- package/crates/aihu-css-core/src/features/mod.rs +33 -0
- package/crates/aihu-css-core/src/features/popover.rs +40 -0
- package/crates/aihu-css-core/src/features/text_balance.rs +36 -0
- package/crates/aihu-css-core/src/features/view_transition.rs +38 -0
- package/crates/aihu-css-core/src/lib.rs +67 -0
- package/crates/aihu-css-core/src/progressive.rs +200 -0
- package/crates/aihu-css-core/src/scanner.rs +235 -0
- package/crates/aihu-css-core/src/theme.rs +179 -0
- package/crates/aihu-css-core/src/tokens.rs +470 -0
- package/crates/aihu-css-core/src/variants.rs +124 -0
- package/crates/aihu-css-core/tests/cache.rs +71 -0
- package/crates/aihu-css-core/tests/emit.rs +148 -0
- package/crates/aihu-css-core/tests/fixtures/button.ast.json +19 -0
- package/crates/aihu-css-core/tests/progressive_snapshot.rs +102 -0
- package/crates/aihu-css-core/tests/scanner.rs +99 -0
- package/crates/aihu-css-core/tests/scoped_snapshot.rs +73 -0
- package/crates/aihu-css-core/tests/snapshot.rs +24 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__anchor_snapshot.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__popover_snapshot.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__text_balance_snapshot.snap +23 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__view_transition_snapshot.snap +25 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__flat_output_for_class_list.snap +6 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_output_for_sfc.snap +25 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_authored_style_block.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_global_style_block.snap +24 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__standard_variants.snap +33 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__theme_default_vs_override.snap +45 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__wc_native_variants.snap +28 -0
- package/crates/aihu-css-core/tests/snapshots/snapshot__compiles_basic_class.snap +5 -0
- package/crates/aihu-css-core/tests/snapshots/snapshot__compiles_multiple_classes.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__arbitrary_values.snap +9 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_borders.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_colors.snap +10 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_effects.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_layout.snap +12 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_spacing.snap +11 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_typography.snap +11 -0
- package/crates/aihu-css-core/tests/tokens.rs +79 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/cn.d.ts +14 -0
- package/dist/runtime/cn.d.ts.map +1 -0
- package/dist/runtime/cn.js +107 -0
- package/dist/runtime/cn.js.map +1 -0
- package/dist/runtime/progressive.d.ts +54 -0
- package/dist/runtime/progressive.d.ts.map +1 -0
- package/dist/runtime/progressive.js +132 -0
- package/dist/runtime/progressive.js.map +1 -0
- package/package.json +54 -0
- package/styles/aihu-default.css +73 -0
- package/styles/aihu-graphite.css +71 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//! Scoped-output, variant, and @theme emission tests (Plan 2 Tasks 4–7).
|
|
2
|
+
|
|
3
|
+
use aihu_css_core::{compile_sfc_scoped, parse_ast};
|
|
4
|
+
|
|
5
|
+
fn ast(json: &str) -> aihu_css_core::SfcAst {
|
|
6
|
+
parse_ast(json).unwrap()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/// Build a single-element SFC AST with a static class list.
|
|
10
|
+
fn sfc_with_classes(classes: &str) -> aihu_css_core::SfcAst {
|
|
11
|
+
let json = format!(
|
|
12
|
+
r#"{{"tag":"X","astVersion":1,"style":null,"meta":{{"name":"X"}},
|
|
13
|
+
"template":[{{"kind":"element","tag":"div","attrs":[
|
|
14
|
+
{{"kind":"static","name":"class","value":"{classes}"}}
|
|
15
|
+
],"children":[]}}]}}"#
|
|
16
|
+
);
|
|
17
|
+
ast(&json)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Task 4: scoped output, no global stylesheet ──────────────────────────────
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn scoped_output_has_no_bare_global_utility_sheet() {
|
|
24
|
+
let css = compile_sfc_scoped(&sfc_with_classes("bg-primary p-4"));
|
|
25
|
+
// The utility rules are class selectors inside the shadow <style>; they are
|
|
26
|
+
// scoped by living in the shadow root. There is no separate global sheet.
|
|
27
|
+
assert!(css.contains(".bg-primary"));
|
|
28
|
+
assert!(css.contains("padding: 1rem"));
|
|
29
|
+
// Theme tokens emitted at :host so var(--color-*) resolves in the shadow.
|
|
30
|
+
assert!(css.contains(":host {"));
|
|
31
|
+
assert!(css.contains("--color-primary"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn scoped_folds_authored_style_block() {
|
|
36
|
+
let json = r#"{"tag":"X","astVersion":1,
|
|
37
|
+
"style":{"content":".extra { color: red; }","scope":"scoped"},
|
|
38
|
+
"meta":{"name":"X"},
|
|
39
|
+
"template":[{"kind":"element","tag":"div","attrs":[
|
|
40
|
+
{"kind":"static","name":"class","value":"p-4"}],"children":[]}]}"#;
|
|
41
|
+
let css = compile_sfc_scoped(&ast(json));
|
|
42
|
+
assert!(css.contains(".p-4"));
|
|
43
|
+
assert!(css.contains(".extra { color: red; }"), "authored scoped @style folded in");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn global_style_block_passes_through_edge_e6() {
|
|
48
|
+
let json = r#"{"tag":"X","astVersion":1,
|
|
49
|
+
"style":{"content":"body { margin: 0; }","scope":"global"},
|
|
50
|
+
"meta":{"name":"X"},"template":null}"#;
|
|
51
|
+
let css = compile_sfc_scoped(&ast(json));
|
|
52
|
+
assert!(css.contains("body { margin: 0; }"));
|
|
53
|
+
assert!(css.contains("unscoped"), "global block annotated as unscoped");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Task 5: WC-native variants ───────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn host_variant_emits_host_rule() {
|
|
60
|
+
let css = compile_sfc_scoped(&sfc_with_classes("host:bg-primary"));
|
|
61
|
+
assert!(css.contains(":host("), "host: → :host(...) selector: {css}");
|
|
62
|
+
assert!(css.contains("background-color: var(--color-primary)"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn slotted_variants() {
|
|
67
|
+
let css = compile_sfc_scoped(&sfc_with_classes("slotted:p-4 slotted-img:rounded-lg"));
|
|
68
|
+
assert!(css.contains("::slotted("));
|
|
69
|
+
assert!(css.contains("::slotted(img"));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn part_variant_emits_part_selector() {
|
|
74
|
+
let css = compile_sfc_scoped(&sfc_with_classes("part-thumb:bg-accent"));
|
|
75
|
+
assert!(css.contains("::part(thumb)"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[test]
|
|
79
|
+
fn host_context_dark_uses_cascade_not_host_context() {
|
|
80
|
+
let css = compile_sfc_scoped(&sfc_with_classes("host-context-dark:bg-surface"));
|
|
81
|
+
assert!(
|
|
82
|
+
!css.contains(":host-context("),
|
|
83
|
+
"Firefox-safe: must NOT emit :host-context(): {css}"
|
|
84
|
+
);
|
|
85
|
+
assert!(css.contains("dark cascade"), "uses the documented cascade mechanism");
|
|
86
|
+
assert!(css.contains(":root.dark"), "dark value gated on consumer .dark scope");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Task 6: standard variants ────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn hover_variant_appends_pseudo_class() {
|
|
93
|
+
let css = compile_sfc_scoped(&sfc_with_classes("hover:bg-primary"));
|
|
94
|
+
assert!(css.contains(":hover"), "hover: → :hover pseudo-class: {css}");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn dark_variant_no_host_context() {
|
|
99
|
+
let css = compile_sfc_scoped(&sfc_with_classes("dark:bg-surface"));
|
|
100
|
+
assert!(!css.contains(":host-context("), "dark: must NOT emit :host-context()");
|
|
101
|
+
assert!(css.contains("dark cascade"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn responsive_md_wraps_media_query() {
|
|
106
|
+
let css = compile_sfc_scoped(&sfc_with_classes("md:p-8"));
|
|
107
|
+
assert!(css.contains("@media (min-width:"), "md: → @media: {css}");
|
|
108
|
+
assert!(css.contains("padding: 2rem"));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn arbitrary_selector_variant() {
|
|
113
|
+
let css = compile_sfc_scoped(&sfc_with_classes("[&>div]:text-primary"));
|
|
114
|
+
assert!(css.contains(">div"), "[&>div]: → child selector: {css}");
|
|
115
|
+
assert!(css.contains("color: var(--color-primary)"));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn stacked_variants_md_hover() {
|
|
120
|
+
let css = compile_sfc_scoped(&sfc_with_classes("md:hover:bg-primary"));
|
|
121
|
+
assert!(css.contains("@media (min-width:"));
|
|
122
|
+
assert!(css.contains(":hover"));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Task 7: @theme directive ─────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn theme_override_registers_token() {
|
|
129
|
+
let json = r#"{"tag":"X","astVersion":1,
|
|
130
|
+
"style":{"content":"@theme { --color-primary: oklch(0.7 0.2 30); }","scope":"scoped"},
|
|
131
|
+
"meta":{"name":"X"},
|
|
132
|
+
"template":[{"kind":"element","tag":"div","attrs":[
|
|
133
|
+
{"kind":"static","name":"class","value":"bg-primary"}],"children":[]}]}"#;
|
|
134
|
+
let css = compile_sfc_scoped(&ast(json));
|
|
135
|
+
// The override value is registered as a :host token...
|
|
136
|
+
assert!(css.contains("--color-primary: oklch(0.7 0.2 30)"), "@theme override registered: {css}");
|
|
137
|
+
// ...and the utility references it.
|
|
138
|
+
assert!(css.contains("background-color: var(--color-primary)"));
|
|
139
|
+
// The @theme directive itself is NOT emitted as raw CSS.
|
|
140
|
+
assert!(!css.contains("@theme"), "@theme directive stripped from raw output");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn default_aihu_brand_tokens_present() {
|
|
145
|
+
let css = compile_sfc_scoped(&sfc_with_classes("bg-accent"));
|
|
146
|
+
// Default accent is the aihu terracotta.
|
|
147
|
+
assert!(css.contains("--color-accent: #c8543a"), "baked aihu brand default present: {css}");
|
|
148
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tag": "Button",
|
|
3
|
+
"astVersion": 1,
|
|
4
|
+
"style": null,
|
|
5
|
+
"template": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "element",
|
|
8
|
+
"tag": "button",
|
|
9
|
+
"attrs": [
|
|
10
|
+
{ "kind": "static", "name": "class", "value": "base" },
|
|
11
|
+
{ "kind": "binding", "name": "class", "expr": "cn('btn', size)" },
|
|
12
|
+
{ "kind": "macro", "name": "class:loading", "value": { "form": "curly", "expr": "busy" } },
|
|
13
|
+
{ "kind": "macro", "name": "on:click", "value": { "form": "curly", "expr": "go" } }
|
|
14
|
+
],
|
|
15
|
+
"children": [{ "kind": "text", "value": "Go" }]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"meta": { "name": "Button" }
|
|
19
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//! Snapshot + behavior coverage for the progressive features (Plan 3 Tasks 5–8).
|
|
2
|
+
//!
|
|
3
|
+
//! Asserts each feature's `@supports`/fallback contract via `compile_sfc_scoped`
|
|
4
|
+
//! (the production path that routes progressive prefixes through the registry).
|
|
5
|
+
|
|
6
|
+
use aihu_css_core::{compile_sfc_scoped, parse_ast};
|
|
7
|
+
|
|
8
|
+
fn ast(json: &str) -> aihu_css_core::SfcAst {
|
|
9
|
+
parse_ast(json).unwrap()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fn sfc(classes: &str) -> aihu_css_core::SfcAst {
|
|
13
|
+
ast(&format!(
|
|
14
|
+
r#"{{"tag":"X","astVersion":1,"style":null,"meta":{{"name":"X"}},
|
|
15
|
+
"template":[{{"kind":"element","tag":"div","attrs":[
|
|
16
|
+
{{"kind":"static","name":"class","value":"{classes}"}}
|
|
17
|
+
],"children":[]}}]}}"#
|
|
18
|
+
))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── Task 5: view-transition: (CSS-only, no JS) ───────────────────────────────
|
|
22
|
+
|
|
23
|
+
#[test]
|
|
24
|
+
fn view_transition_is_supports_gated_css_only() {
|
|
25
|
+
let css = compile_sfc_scoped(&sfc("view-transition:hero"));
|
|
26
|
+
assert!(
|
|
27
|
+
css.contains("@supports (view-transition-name: none)"),
|
|
28
|
+
"view-transition gated behind @supports: {css}"
|
|
29
|
+
);
|
|
30
|
+
assert!(css.contains("view-transition-name: hero"));
|
|
31
|
+
assert!(
|
|
32
|
+
!css.contains("aihu:progressive-fallback"),
|
|
33
|
+
"view-transition is CSS-only — NO JS fallback marker: {css}"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn view_transition_snapshot() {
|
|
39
|
+
insta::assert_snapshot!(compile_sfc_scoped(&sfc("view-transition:hero")));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Task 6: anchor: (@supports gate + JS fallback marker) ────────────────────
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn anchor_is_supports_gated_with_js_marker() {
|
|
46
|
+
let css = compile_sfc_scoped(&sfc("anchor:tooltip"));
|
|
47
|
+
assert!(
|
|
48
|
+
css.contains("@supports (anchor-name: --a)"),
|
|
49
|
+
"anchor gated behind @supports (anchor-name): {css}"
|
|
50
|
+
);
|
|
51
|
+
assert!(css.contains("anchor-name: --tooltip"));
|
|
52
|
+
assert!(
|
|
53
|
+
css.contains("aihu:progressive-fallback anchorFallback"),
|
|
54
|
+
"anchor emits a runtime-fallback marker: {css}"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn anchor_snapshot() {
|
|
60
|
+
insta::assert_snapshot!(compile_sfc_scoped(&sfc("anchor:tooltip")));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Task 7: popover: (@supports gate + portal JS fallback marker) ────────────
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn popover_is_supports_gated_with_js_marker() {
|
|
67
|
+
let css = compile_sfc_scoped(&sfc("popover:menu"));
|
|
68
|
+
assert!(
|
|
69
|
+
css.contains("@supports (selector(:popover-open))"),
|
|
70
|
+
"popover gated behind @supports selector(:popover-open): {css}"
|
|
71
|
+
);
|
|
72
|
+
assert!(
|
|
73
|
+
css.contains("aihu:progressive-fallback popoverFallback"),
|
|
74
|
+
"popover emits a runtime-fallback marker: {css}"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[test]
|
|
79
|
+
fn popover_snapshot() {
|
|
80
|
+
insta::assert_snapshot!(compile_sfc_scoped(&sfc("popover:menu")));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Task 8: text-balance: (no gate, no JS) ───────────────────────────────────
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn text_balance_no_gate_no_js() {
|
|
87
|
+
let css = compile_sfc_scoped(&sfc("text-balance:"));
|
|
88
|
+
assert!(css.contains("text-wrap: balance"), "emits text-wrap: balance: {css}");
|
|
89
|
+
assert!(
|
|
90
|
+
!css.contains("@supports"),
|
|
91
|
+
"text-balance has NO @supports gate (silently ignored if unsupported): {css}"
|
|
92
|
+
);
|
|
93
|
+
assert!(
|
|
94
|
+
!css.contains("aihu:progressive-fallback"),
|
|
95
|
+
"text-balance is CSS-only — NO JS fallback marker: {css}"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn text_balance_snapshot() {
|
|
101
|
+
insta::assert_snapshot!(compile_sfc_scoped(&sfc("text-balance:")));
|
|
102
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//! Scanner tests — proves the three class-forms (Static / Binding / Macro) are
|
|
2
|
+
//! distinguished and that non-class macros never enter the utility set. The
|
|
3
|
+
//! fixture is REAL `aihu-compile --ast-json` output (not hand-authored).
|
|
4
|
+
|
|
5
|
+
use aihu_css_core::{parse_ast, scan, scan_ast, SfcAst};
|
|
6
|
+
|
|
7
|
+
fn fixture_ast() -> SfcAst {
|
|
8
|
+
// From `aihu-compile --ast-json` for:
|
|
9
|
+
// <button class="base" $class={cn('btn', size)} $class:loading={busy} $on.click={go}>Go</button>
|
|
10
|
+
parse_ast(include_str!("fixtures/button.ast.json")).unwrap()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[test]
|
|
14
|
+
fn extracts_static_form_a() {
|
|
15
|
+
let set = scan_ast(&fixture_ast());
|
|
16
|
+
assert!(set.contains("base"), "Form A static class missing: {set:?}");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn extracts_binding_string_literals_form_b() {
|
|
21
|
+
let set = scan_ast(&fixture_ast());
|
|
22
|
+
assert!(set.contains("btn"), "string literal in cn('btn', size) missing");
|
|
23
|
+
assert!(!set.contains("size"), "bare identifier `size` must NOT be a utility");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn flags_unresolved_binding_identifiers() {
|
|
28
|
+
let result = scan(&fixture_ast());
|
|
29
|
+
assert!(
|
|
30
|
+
result.unresolved.contains("size"),
|
|
31
|
+
"unresolved identifier `size` should be flagged for diagnostics: {:?}",
|
|
32
|
+
result.unresolved
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn extracts_macro_class_toggle_form_c() {
|
|
38
|
+
let set = scan_ast(&fixture_ast());
|
|
39
|
+
assert!(set.contains("loading"), "Form C $class:loading toggle missing");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[test]
|
|
43
|
+
fn skips_non_class_macros() {
|
|
44
|
+
// $on.click / $if must never enter the utility set.
|
|
45
|
+
let set = scan_ast(&fixture_ast());
|
|
46
|
+
assert!(
|
|
47
|
+
!set.iter().any(|c| c.contains("click") || c == "if" || c.contains("on:")),
|
|
48
|
+
"non-class macro leaked into utility set: {set:?}"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn rejects_unsupported_ast_version() {
|
|
54
|
+
let json = r#"{"tag":"X","astVersion":2,"style":null,"template":null,"meta":{"name":"X"}}"#;
|
|
55
|
+
assert!(parse_ast(json).is_err(), "astVersion 2 must be rejected");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn empty_template_yields_empty_set() {
|
|
60
|
+
let json = r#"{"tag":"X","astVersion":1,"style":null,"template":null,"meta":{"name":"X"}}"#;
|
|
61
|
+
let ast = parse_ast(json).unwrap();
|
|
62
|
+
assert!(scan_ast(&ast).is_empty(), "no @template → empty utility set (edge E5)");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn skips_component_node_class_attrs_edge_e10() {
|
|
67
|
+
// A macroElement (component) node owns its own shadow scope; its class
|
|
68
|
+
// attrs must not be compiled into the parent sheet.
|
|
69
|
+
let json = r#"{
|
|
70
|
+
"tag":"Parent","astVersion":1,"style":null,"meta":{"name":"Parent"},
|
|
71
|
+
"template":[
|
|
72
|
+
{"kind":"macroElement","name":"UserCard","attrs":[
|
|
73
|
+
{"kind":"static","name":"class","value":"should-not-appear"}
|
|
74
|
+
],"children":[
|
|
75
|
+
{"kind":"element","tag":"span","attrs":[
|
|
76
|
+
{"kind":"static","name":"class","value":"slotted-content"}
|
|
77
|
+
],"children":[]}
|
|
78
|
+
]}
|
|
79
|
+
]}"#;
|
|
80
|
+
let ast = parse_ast(json).unwrap();
|
|
81
|
+
let set = scan_ast(&ast);
|
|
82
|
+
assert!(!set.contains("should-not-appear"), "component class must be skipped (E10)");
|
|
83
|
+
assert!(set.contains("slotted-content"), "child HTML element class should still scan");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn array_binding_extracts_literals_edge_e2() {
|
|
88
|
+
// $class={['a', cond && 'b']} → 'a','b' utilities, `cond` unresolved.
|
|
89
|
+
let json = r#"{
|
|
90
|
+
"tag":"X","astVersion":1,"style":null,"meta":{"name":"X"},
|
|
91
|
+
"template":[{"kind":"element","tag":"div","attrs":[
|
|
92
|
+
{"kind":"binding","name":"class","expr":"['a', cond && 'b']"}
|
|
93
|
+
],"children":[]}]}"#;
|
|
94
|
+
let ast = parse_ast(json).unwrap();
|
|
95
|
+
let result = scan(&ast);
|
|
96
|
+
assert!(result.utilities.contains("a"));
|
|
97
|
+
assert!(result.utilities.contains("b"));
|
|
98
|
+
assert!(result.unresolved.contains("cond"));
|
|
99
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//! Snapshot coverage for scoped output + variants + @theme (Plan 2 Tasks 4–7).
|
|
2
|
+
|
|
3
|
+
use aihu_css_core::{compile_classes, compile_sfc_scoped, parse_ast};
|
|
4
|
+
|
|
5
|
+
fn ast(json: &str) -> aihu_css_core::SfcAst {
|
|
6
|
+
parse_ast(json).unwrap()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
fn sfc(classes: &str) -> aihu_css_core::SfcAst {
|
|
10
|
+
ast(&format!(
|
|
11
|
+
r#"{{"tag":"X","astVersion":1,"style":null,"meta":{{"name":"X"}},
|
|
12
|
+
"template":[{{"kind":"element","tag":"div","attrs":[
|
|
13
|
+
{{"kind":"static","name":"class","value":"{classes}"}}
|
|
14
|
+
],"children":[]}}]}}"#
|
|
15
|
+
))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn flat_output_for_class_list() {
|
|
20
|
+
insta::assert_snapshot!(compile_classes(&[
|
|
21
|
+
"bg-primary".to_string(),
|
|
22
|
+
"p-4".to_string()
|
|
23
|
+
]));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn scoped_output_for_sfc() {
|
|
28
|
+
insta::assert_snapshot!(compile_sfc_scoped(&sfc("bg-primary p-4 rounded-lg")));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn scoped_with_authored_style_block() {
|
|
33
|
+
let json = r#"{"tag":"Card","astVersion":1,
|
|
34
|
+
"style":{"content":".inner { display: grid; gap: 1rem; }","scope":"scoped"},
|
|
35
|
+
"meta":{"name":"Card"},
|
|
36
|
+
"template":[{"kind":"element","tag":"div","attrs":[
|
|
37
|
+
{"kind":"static","name":"class","value":"p-4 shadow-md"}],"children":[]}]}"#;
|
|
38
|
+
insta::assert_snapshot!(compile_sfc_scoped(&ast(json)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[test]
|
|
42
|
+
fn scoped_with_global_style_block() {
|
|
43
|
+
let json = r#"{"tag":"X","astVersion":1,
|
|
44
|
+
"style":{"content":"body { margin: 0; }","scope":"global"},
|
|
45
|
+
"meta":{"name":"X"},"template":null}"#;
|
|
46
|
+
insta::assert_snapshot!(compile_sfc_scoped(&ast(json)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[test]
|
|
50
|
+
fn wc_native_variants() {
|
|
51
|
+
insta::assert_snapshot!(compile_sfc_scoped(&sfc(
|
|
52
|
+
"host:bg-primary slotted:p-4 slotted-img:rounded-lg part-thumb:bg-accent host-context-dark:bg-surface"
|
|
53
|
+
)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn standard_variants() {
|
|
58
|
+
insta::assert_snapshot!(compile_sfc_scoped(&sfc(
|
|
59
|
+
"hover:bg-primary focus:text-accent dark:bg-surface md:p-8 [&>div]:text-primary md:hover:bg-primary"
|
|
60
|
+
)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn theme_default_vs_override() {
|
|
65
|
+
let default = compile_sfc_scoped(&sfc("bg-primary"));
|
|
66
|
+
let json = r#"{"tag":"X","astVersion":1,
|
|
67
|
+
"style":{"content":"@theme { --color-primary: oklch(0.55 0.18 28); }","scope":"scoped"},
|
|
68
|
+
"meta":{"name":"X"},
|
|
69
|
+
"template":[{"kind":"element","tag":"div","attrs":[
|
|
70
|
+
{"kind":"static","name":"class","value":"bg-primary"}],"children":[]}]}"#;
|
|
71
|
+
let overridden = compile_sfc_scoped(&ast(json));
|
|
72
|
+
insta::assert_snapshot!(format!("--- default ---\n{default}\n--- override ---\n{overridden}"));
|
|
73
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
use aihu_css_core::compile_classes;
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
fn compiles_basic_class() {
|
|
5
|
+
let output = compile_classes(&["bg-primary".to_string()]);
|
|
6
|
+
insta::assert_snapshot!(output);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#[test]
|
|
10
|
+
fn compiles_multiple_classes() {
|
|
11
|
+
let output = compile_classes(&[
|
|
12
|
+
"bg-primary".to_string(),
|
|
13
|
+
"text-primary-foreground".to_string(),
|
|
14
|
+
"rounded-md".to_string(),
|
|
15
|
+
"p-4".to_string(),
|
|
16
|
+
]);
|
|
17
|
+
insta::assert_snapshot!(output);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[test]
|
|
21
|
+
fn unknown_class_returns_empty_string() {
|
|
22
|
+
let output = compile_classes(&["this-class-does-not-exist".to_string()]);
|
|
23
|
+
assert_eq!(output, "");
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: packages/css-engine/crates/aihu-css-core/tests/progressive_snapshot.rs
|
|
3
|
+
expression: "compile_sfc_scoped(&sfc(\"anchor:tooltip\"))"
|
|
4
|
+
---
|
|
5
|
+
:host {
|
|
6
|
+
--color-accent: #c8543a;
|
|
7
|
+
--color-accent-foreground: #faf8f4;
|
|
8
|
+
--color-background: #faf8f4;
|
|
9
|
+
--color-border: #ddd9d2;
|
|
10
|
+
--color-destructive: #a8432b;
|
|
11
|
+
--color-destructive-foreground: #faf8f4;
|
|
12
|
+
--color-foreground: #1a1d24;
|
|
13
|
+
--color-muted: #5a5a55;
|
|
14
|
+
--color-muted-foreground: #8a8880;
|
|
15
|
+
--color-primary: #1a1d24;
|
|
16
|
+
--color-primary-foreground: #faf8f4;
|
|
17
|
+
--color-ring: #c8543a;
|
|
18
|
+
--color-secondary: #5a5a55;
|
|
19
|
+
--color-secondary-foreground: #faf8f4;
|
|
20
|
+
--color-surface: #faf8f4;
|
|
21
|
+
--color-surface-foreground: #1a1d24;
|
|
22
|
+
}
|
|
23
|
+
@supports (anchor-name: --a) {
|
|
24
|
+
.anchor\:tooltip { anchor-name: --tooltip; position-anchor: --tooltip; }
|
|
25
|
+
}
|
|
26
|
+
/* aihu:progressive-fallback anchorFallback (when not @supports anchor-name: --a) */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: packages/css-engine/crates/aihu-css-core/tests/progressive_snapshot.rs
|
|
3
|
+
expression: "compile_sfc_scoped(&sfc(\"popover:menu\"))"
|
|
4
|
+
---
|
|
5
|
+
:host {
|
|
6
|
+
--color-accent: #c8543a;
|
|
7
|
+
--color-accent-foreground: #faf8f4;
|
|
8
|
+
--color-background: #faf8f4;
|
|
9
|
+
--color-border: #ddd9d2;
|
|
10
|
+
--color-destructive: #a8432b;
|
|
11
|
+
--color-destructive-foreground: #faf8f4;
|
|
12
|
+
--color-foreground: #1a1d24;
|
|
13
|
+
--color-muted: #5a5a55;
|
|
14
|
+
--color-muted-foreground: #8a8880;
|
|
15
|
+
--color-primary: #1a1d24;
|
|
16
|
+
--color-primary-foreground: #faf8f4;
|
|
17
|
+
--color-ring: #c8543a;
|
|
18
|
+
--color-secondary: #5a5a55;
|
|
19
|
+
--color-secondary-foreground: #faf8f4;
|
|
20
|
+
--color-surface: #faf8f4;
|
|
21
|
+
--color-surface-foreground: #1a1d24;
|
|
22
|
+
}
|
|
23
|
+
@supports (selector(:popover-open)) {
|
|
24
|
+
.popover\:menu:popover-open { position: fixed; margin: 0; inset: auto; }
|
|
25
|
+
}
|
|
26
|
+
/* aihu:progressive-fallback popoverFallback (when not @supports selector(:popover-open)) */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: packages/css-engine/crates/aihu-css-core/tests/progressive_snapshot.rs
|
|
3
|
+
expression: "compile_sfc_scoped(&sfc(\"text-balance:\"))"
|
|
4
|
+
---
|
|
5
|
+
:host {
|
|
6
|
+
--color-accent: #c8543a;
|
|
7
|
+
--color-accent-foreground: #faf8f4;
|
|
8
|
+
--color-background: #faf8f4;
|
|
9
|
+
--color-border: #ddd9d2;
|
|
10
|
+
--color-destructive: #a8432b;
|
|
11
|
+
--color-destructive-foreground: #faf8f4;
|
|
12
|
+
--color-foreground: #1a1d24;
|
|
13
|
+
--color-muted: #5a5a55;
|
|
14
|
+
--color-muted-foreground: #8a8880;
|
|
15
|
+
--color-primary: #1a1d24;
|
|
16
|
+
--color-primary-foreground: #faf8f4;
|
|
17
|
+
--color-ring: #c8543a;
|
|
18
|
+
--color-secondary: #5a5a55;
|
|
19
|
+
--color-secondary-foreground: #faf8f4;
|
|
20
|
+
--color-surface: #faf8f4;
|
|
21
|
+
--color-surface-foreground: #1a1d24;
|
|
22
|
+
}
|
|
23
|
+
.text-balance { text-wrap: balance; }
|
package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__view_transition_snapshot.snap
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: packages/css-engine/crates/aihu-css-core/tests/progressive_snapshot.rs
|
|
3
|
+
expression: "compile_sfc_scoped(&sfc(\"view-transition:hero\"))"
|
|
4
|
+
---
|
|
5
|
+
:host {
|
|
6
|
+
--color-accent: #c8543a;
|
|
7
|
+
--color-accent-foreground: #faf8f4;
|
|
8
|
+
--color-background: #faf8f4;
|
|
9
|
+
--color-border: #ddd9d2;
|
|
10
|
+
--color-destructive: #a8432b;
|
|
11
|
+
--color-destructive-foreground: #faf8f4;
|
|
12
|
+
--color-foreground: #1a1d24;
|
|
13
|
+
--color-muted: #5a5a55;
|
|
14
|
+
--color-muted-foreground: #8a8880;
|
|
15
|
+
--color-primary: #1a1d24;
|
|
16
|
+
--color-primary-foreground: #faf8f4;
|
|
17
|
+
--color-ring: #c8543a;
|
|
18
|
+
--color-secondary: #5a5a55;
|
|
19
|
+
--color-secondary-foreground: #faf8f4;
|
|
20
|
+
--color-surface: #faf8f4;
|
|
21
|
+
--color-surface-foreground: #1a1d24;
|
|
22
|
+
}
|
|
23
|
+
@supports (view-transition-name: none) {
|
|
24
|
+
.view-transition\:hero { view-transition-name: hero; }
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
|
|
3
|
+
expression: "compile_sfc_scoped(&sfc(\"bg-primary p-4 rounded-lg\"))"
|
|
4
|
+
---
|
|
5
|
+
:host {
|
|
6
|
+
--color-accent: #c8543a;
|
|
7
|
+
--color-accent-foreground: #faf8f4;
|
|
8
|
+
--color-background: #faf8f4;
|
|
9
|
+
--color-border: #ddd9d2;
|
|
10
|
+
--color-destructive: #a8432b;
|
|
11
|
+
--color-destructive-foreground: #faf8f4;
|
|
12
|
+
--color-foreground: #1a1d24;
|
|
13
|
+
--color-muted: #5a5a55;
|
|
14
|
+
--color-muted-foreground: #8a8880;
|
|
15
|
+
--color-primary: #1a1d24;
|
|
16
|
+
--color-primary-foreground: #faf8f4;
|
|
17
|
+
--color-ring: #c8543a;
|
|
18
|
+
--color-secondary: #5a5a55;
|
|
19
|
+
--color-secondary-foreground: #faf8f4;
|
|
20
|
+
--color-surface: #faf8f4;
|
|
21
|
+
--color-surface-foreground: #1a1d24;
|
|
22
|
+
}
|
|
23
|
+
.bg-primary { background-color: var(--color-primary); }
|
|
24
|
+
.p-4 { padding: 1rem; }
|
|
25
|
+
.rounded-lg { border-radius: 0.5rem; }
|
package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_authored_style_block.snap
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
|
|
3
|
+
expression: compile_sfc_scoped(&ast(json))
|
|
4
|
+
---
|
|
5
|
+
:host {
|
|
6
|
+
--color-accent: #c8543a;
|
|
7
|
+
--color-accent-foreground: #faf8f4;
|
|
8
|
+
--color-background: #faf8f4;
|
|
9
|
+
--color-border: #ddd9d2;
|
|
10
|
+
--color-destructive: #a8432b;
|
|
11
|
+
--color-destructive-foreground: #faf8f4;
|
|
12
|
+
--color-foreground: #1a1d24;
|
|
13
|
+
--color-muted: #5a5a55;
|
|
14
|
+
--color-muted-foreground: #8a8880;
|
|
15
|
+
--color-primary: #1a1d24;
|
|
16
|
+
--color-primary-foreground: #faf8f4;
|
|
17
|
+
--color-ring: #c8543a;
|
|
18
|
+
--color-secondary: #5a5a55;
|
|
19
|
+
--color-secondary-foreground: #faf8f4;
|
|
20
|
+
--color-surface: #faf8f4;
|
|
21
|
+
--color-surface-foreground: #1a1d24;
|
|
22
|
+
}
|
|
23
|
+
.p-4 { padding: 1rem; }
|
|
24
|
+
.shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
|
|
25
|
+
/* authored @style (scoped) */
|
|
26
|
+
.inner { display: grid; gap: 1rem; }
|
package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_global_style_block.snap
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
|
|
3
|
+
expression: compile_sfc_scoped(&ast(json))
|
|
4
|
+
---
|
|
5
|
+
:host {
|
|
6
|
+
--color-accent: #c8543a;
|
|
7
|
+
--color-accent-foreground: #faf8f4;
|
|
8
|
+
--color-background: #faf8f4;
|
|
9
|
+
--color-border: #ddd9d2;
|
|
10
|
+
--color-destructive: #a8432b;
|
|
11
|
+
--color-destructive-foreground: #faf8f4;
|
|
12
|
+
--color-foreground: #1a1d24;
|
|
13
|
+
--color-muted: #5a5a55;
|
|
14
|
+
--color-muted-foreground: #8a8880;
|
|
15
|
+
--color-primary: #1a1d24;
|
|
16
|
+
--color-primary-foreground: #faf8f4;
|
|
17
|
+
--color-ring: #c8543a;
|
|
18
|
+
--color-secondary: #5a5a55;
|
|
19
|
+
--color-secondary-foreground: #faf8f4;
|
|
20
|
+
--color-surface: #faf8f4;
|
|
21
|
+
--color-surface-foreground: #1a1d24;
|
|
22
|
+
}
|
|
23
|
+
/* authored @style ($global — unscoped) */
|
|
24
|
+
body { margin: 0; }
|