Skip to main content

atmos/os_lib/layout/
selftest.rs

1// 分割: layout.rs より機械的に移動(2026-07-16 リファクタ フェーズ4)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// レイアウト寸法のセルフテスト。html+css を固定ビューポート幅でレイアウトし、
6/// 指定 id の content 矩形 (x,y,w,h) を検証する。CSS レイアウト作業の回帰防止基盤。
7pub fn selftest() -> (usize, usize) {
8    fn box_id<'a>(b: &LayoutBox<'a>) -> Option<&'a str> {
9        let styled = match &b.box_type {
10            BoxType::BlockNode(s)
11            | BoxType::InlineNode(s)
12            | BoxType::InlineBlockNode(s)
13            | BoxType::FlexNode(s)
14            | BoxType::GridNode(s)
15            | BoxType::TableNode(s)
16            | BoxType::TableRowGroupNode(s)
17            | BoxType::TableRowNode(s)
18            | BoxType::TableCellNode(s) => *s,
19            BoxType::AnonymousBlock => return None,
20        };
21        match &styled.node.node_type {
22            NodeType::Element { id, .. } => id.as_deref(),
23            NodeType::Text(_) => None,
24        }
25    }
26    fn find<'a, 'b>(b: &'b LayoutBox<'a>, id: &str) -> Option<&'b LayoutBox<'a>> {
27        if box_id(b) == Some(id) {
28            return Some(b);
29        }
30        for c in &b.children {
31            if let Some(r) = find(c, id) {
32                return Some(r);
33            }
34        }
35        None
36    }
37    fn measure(html: &str, css: &str, vw: i32, id: &str) -> Option<(i32, i32, i32, i32)> {
38        let dom = super::super::dom::parse_html(html);
39        let sheet = super::super::css::parse_css(css);
40        let styled = super::super::css::style_tree(&dom, &sheet);
41        set_viewport_hint(vw, 600);
42        let icb = Dimensions {
43            content: Rect {
44                x: 0,
45                y: 0,
46                width: vw,
47                height: 0,
48            },
49            ..Default::default()
50        };
51        let tree = layout_tree(&styled, icb);
52        let b = find(&tree, id)?;
53        let c = &b.dimensions.content;
54        Some((c.x, c.y, c.width, c.height))
55    }
56
57    // (html, css, viewport_w, id, expected (x,y,w,h))
58    let cases: &[(&str, &str, i32, &str, (i32, i32, i32, i32))] = &[
59        // 1. flex row, 既定 (flex-start)
60        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
61         "#f{display:flex} #a{width:30px;height:10px} #b{width:40px;height:10px}", 200, "a", (0, 0, 30, 10)),
62        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
63         "#f{display:flex} #a{width:30px;height:10px} #b{width:40px;height:10px}", 200, "b", (30, 0, 40, 10)),
64        // 2. justify-content:center → (200-50)/2 = 75
65        ("<div id='f'><div id='a'></div></div>",
66         "#f{display:flex;justify-content:center} #a{width:50px;height:10px}", 200, "a", (75, 0, 50, 10)),
67        // 3. justify-content:flex-end → 200-50 = 150
68        ("<div id='f'><div id='a'></div></div>",
69         "#f{display:flex;justify-content:flex-end} #a{width:50px;height:10px}", 200, "a", (150, 0, 50, 10)),
70        // 4. justify-content:space-between → b at 200-20 = 180
71        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
72         "#f{display:flex;justify-content:space-between} #a{width:20px;height:10px} #b{width:20px;height:10px}", 200, "b", (180, 0, 20, 10)),
73        // 4b. justify-content:space-evenly(以前は未対応で flex-start と同じ挙動に落ちる
74        // バグだった)。main_free=200-40=160, n=2 → offset/間隔=160/3=53。
75        // a: x=53。b: x=53+20+53=126。
76        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
77         "#f{display:flex;justify-content:space-evenly} #a{width:20px;height:10px} #b{width:20px;height:10px}", 200, "a", (53, 0, 20, 10)),
78        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
79         "#f{display:flex;justify-content:space-evenly} #a{width:20px;height:10px} #b{width:20px;height:10px}", 200, "b", (126, 0, 20, 10)),
80        // 4d. justify-content:end(CSS Box Alignment Level 3 の flex-end 別名。以前は
81        // 未対応で flex-start と同じ挙動に落ちるバグだった)→ flex-end と同じ 150。
82        ("<div id='f'><div id='a'></div></div>",
83         "#f{display:flex;justify-content:end} #a{width:50px;height:10px}", 200, "a", (150, 0, 50, 10)),
84        // 4e. align-items:end(同様に flex-end 別名。以前は stretch/flex-start と
85        // 同じ挙動に落ちるバグだった)→ align-items:flex-end と同じ y=80。
86        ("<div id='f'><div id='a'></div></div>",
87         "#f{display:flex;height:100px;align-items:end} #a{width:30px;height:20px}", 200, "a", (0, 80, 30, 20)),
88        // 4f. align-self:self-end(同様の別名)。
89        ("<div id='f'><div id='a'></div></div>",
90         "#f{display:flex;height:100px} #a{width:30px;height:20px;align-self:self-end}", 200, "a", (0, 80, 30, 20)),
91        // 4g. flex-wrap:wrap 側の行内 justify-content 実装(別コードパス)でも
92        // end 別名が効くことを確認。
93        ("<div id='f'><div id='a'></div></div>",
94         "#f{display:flex;flex-wrap:wrap;justify-content:end} #a{width:50px;height:10px}", 200, "a", (150, 0, 50, 10)),
95        // 4c. flex-wrap:wrap 側の行内 justify-content 実装(別コードパス)でも
96        // space-evenly が同じ規則で効くことを確認(1行に収まるので単一行と同じ結果)。
97        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
98         "#f{display:flex;flex-wrap:wrap;justify-content:space-evenly} #a{width:20px;height:10px} #b{width:20px;height:10px}", 200, "b", (126, 0, 20, 10)),
99        // 5. column direction → b stacks below a (y = a.height = 10)
100        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
101         "#f{display:flex;flex-direction:column} #a{width:30px;height:10px} #b{width:30px;height:15px}", 200, "b", (0, 10, 30, 15)),
102        // 6. align-items:center, container height 100, a height 20 → y = (100-20)/2 = 40
103        ("<div id='f'><div id='a'></div></div>",
104         "#f{display:flex;align-items:center;height:100px} #a{width:30px;height:20px}", 200, "a", (0, 40, 30, 20)),
105        // 7. gap:10px → b at a.width(20)+gap(10) = 30
106        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
107         "#f{display:flex;gap:10px} #a{width:20px;height:10px} #b{width:20px;height:10px}", 200, "b", (30, 0, 20, 10)),
108        // 8. block honors explicit height
109        ("<div id='t'></div>", "#t{height:10px}", 200, "t", (0, 0, 200, 10)),
110        // 9. align-items:stretch (既定): 高さ未指定の子がコンテナ高 100 へ伸びる
111        ("<div id='f'><div id='a'></div></div>",
112         "#f{display:flex;height:100px} #a{width:30px}", 200, "a", (0, 0, 30, 100)),
113        // 10. stretch しない: 子に明示 height があれば尊重
114        ("<div id='f'><div id='a'></div></div>",
115         "#f{display:flex;height:100px} #a{width:30px;height:20px}", 200, "a", (0, 0, 30, 20)),
116        // 11. column + stretch: 幅未指定の子がコンテナ幅 200 へ伸びる
117        ("<div id='f'><div id='a'></div></div>",
118         "#f{display:flex;flex-direction:column} #a{height:10px}", 200, "a", (0, 0, 200, 10)),
119        // 12. flex:1 を2つ → 200 を等分 (各 100)
120        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
121         "#f{display:flex} #a{flex:1;height:10px} #b{flex:1;height:10px}", 200, "a", (0, 0, 100, 10)),
122        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
123         "#f{display:flex} #a{flex:1;height:10px} #b{flex:1;height:10px}", 200, "b", (100, 0, 100, 10)),
124        // 13. 固定 50 + flex:1 → grower が残り 150 を埋める
125        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
126         "#f{display:flex} #a{width:50px;height:10px} #b{flex:1;height:10px}", 200, "b", (50, 0, 150, 10)),
127        // 14. grow 比 1:2 → 66 / 134(端数は最後の grower へ)
128        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
129         "#f{display:flex} #a{flex-grow:1;height:10px} #b{flex-grow:2;height:10px}", 200, "b", (66, 0, 134, 10)),
130        // 15. column + flex:1 → 高さ100 内で残余 80 を grower が埋める、b は y=80
131        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
132         "#f{display:flex;flex-direction:column;height:100px} #a{flex:1;width:30px} #b{height:20px;width:30px}", 200, "b", (0, 80, 30, 20)),
133        // 16. position:relative → フロー位置(0,10)から left/top オフセット
134        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
135         "#a{height:10px} #b{height:10px;position:relative;left:20px;top:5px}", 200, "b", (20, 15, 200, 10)),
136        // 17. relative は静的兄弟 a に影響しない
137        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
138         "#a{height:10px} #b{height:10px;position:relative;left:20px;top:5px}", 200, "a", (0, 0, 200, 10)),
139        // 18. position:absolute → relative親基準で top/left 配置
140        ("<div id='p'><div id='a'></div></div>",
141         "#p{position:relative;height:100px} #a{position:absolute;top:10px;left:30px;width:40px;height:20px}", 200, "a", (30, 10, 40, 20)),
142        // 18b. `inset` ショートハンド(4値): top/right/bottom/left を一括指定。
143        // longhand と同じ結果になることを確認。
144        ("<div id='p'><div id='a'></div></div>",
145         "#p{position:relative;height:100px} #a{position:absolute;inset:10px 0 0 30px;width:40px;height:20px}", 200, "a", (30, 10, 40, 20)),
146        // 18c. `inset` ショートハンド(1値): 全辺 0 → 親の左上に密着。
147        ("<div id='p'><div id='a'></div></div>",
148         "#p{position:relative;height:100px} #a{position:absolute;inset:0;width:40px;height:20px}", 200, "a", (0, 0, 40, 20)),
149        // 18d. longhand(left/top)が明示されていれば inset ショートハンドより優先される。
150        ("<div id='p'><div id='a'></div></div>",
151         "#p{position:relative;height:100px} #a{position:absolute;inset:0;left:15px;width:40px;height:20px}", 200, "a", (15, 0, 40, 20)),
152        // 19. absolute right/bottom → 200x100 の右下基準(200-10-40=150, 100-5-20=75)
153        ("<div id='p'><div id='a'></div></div>",
154         "#p{position:relative;width:200px;height:100px} #a{position:absolute;right:10px;bottom:5px;width:40px;height:20px}", 200, "a", (150, 75, 40, 20)),
155        // 20. absolute はフロー外 → 静的兄弟 b は y=0 のまま
156        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
157         "#p{position:relative} #a{position:absolute;top:0;left:0;width:50px;height:50px} #b{height:10px}", 200, "b", (0, 0, 200, 10)),
158        // 21. absolute は最近傍 positioned 祖先(#gp)まで昇る(static の #wrap は飛ばす)。
159        //     #sp で #wrap を y=30 に押下げても #a は #gp 原点基準 → (20,10) であって (20,40) ではない
160        ("<div id='gp'><div id='sp'></div><div id='wrap'><div id='a'></div></div></div>",
161         "#gp{position:relative;height:100px} #sp{height:30px} #a{position:absolute;top:10px;left:20px;width:30px;height:15px}", 200, "a", (20, 10, 30, 15)),
162        // 22. fixed はビューポート原点基準(このテストの ICB 原点 = (0,0))
163        ("<div id='h'></div>",
164         "#h{position:fixed;top:5px;left:8px;width:100px;height:20px}", 200, "h", (8, 5, 100, 20)),
165        // 23. transform: translate(px) → フロー位置(0,10)から (10,5) シフト
166        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
167         "#a{height:10px} #b{height:10px;transform:translate(10px,5px)}", 200, "b", (10, 15, 200, 10)),
168        // 24. transform は静的兄弟 a を動かさない(再フロー無し)
169        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
170         "#a{height:10px} #b{height:10px;transform:translate(10px,5px)}", 200, "a", (0, 0, 200, 10)),
171        // 25. translateX のみ
172        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
173         "#a{height:10px} #b{height:10px;transform:translateX(20px)}", 200, "b", (20, 10, 200, 10)),
174        // 26. translate(%) は要素自身の寸法基準(40x10 → 50%=20, 100%=10)
175        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
176         "#a{height:10px} #b{width:40px;height:10px;transform:translate(50%,100%)}", 200, "b", (20, 20, 40, 10)),
177        // 27. display:none は a を flow から除外 → b は y=0
178        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
179         "#a{display:none;height:10px} #b{height:10px}", 200, "b", (0, 0, 200, 10)),
180        // 28. box-sizing:border-box → content幅 = 100 - padding(10+10) = 80、x=padding.left=10
181        ("<div id='t'></div>",
182         "#t{width:100px;padding:0 10px;box-sizing:border-box;height:30px}", 200, "t", (10, 0, 80, 30)),
183        // 29. margin:0 auto → 幅100をコンテナ200内で水平センタリング(左margin=50)
184        ("<div id='p'><div id='a'></div></div>",
185         "#a{width:100px;margin:0 auto;height:10px}", 200, "a", (50, 0, 100, 10)),
186        // 30. box-sizing:border-box の height → 指定50から縦padding20を引いて content高30、y=padding.top=10
187        ("<div id='t'></div>",
188         "#t{width:50px;height:50px;padding:10px 0;box-sizing:border-box}", 200, "t", (0, 10, 50, 30)),
189        // 31. vh 単位 → 100vh=VIEWPORT_HINT_H デフォルト600
190        ("<div id='t'></div>", "#t{height:100vh}", 200, "t", (0, 0, 200, 600)),
191        // 32. rem 単位 → 2rem=32px(base 16px)
192        ("<div id='t'></div>", "#t{height:2rem}", 200, "t", (0, 0, 200, 32)),
193        // 33. CSS Grid repeat(3,1fr) → 3列等幅 198/3=66
194        ("<div id='g'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
195         "#g{display:grid;grid-template-columns:repeat(3,1fr)} #a,#b,#c{height:10px}", 198, "b", (66, 0, 66, 10)),
196        // 33-b. CSS 多段組 column-count:2(gap 既定 16px)
197        //   幅 216 → usable = 216-16 = 200 → 各列 100px。
198        //   子は高さの低い列から順に入るので b は 2 列目(x=116)の先頭。
199        ("<div id='m'><div id='a'></div><div id='b'></div></div>",
200         "#m{column-count:2} #a,#b{height:10px}", 216, "b", (116, 0, 100, 10)),
201        // 33-c. 多段組は積んだ順に高さ最小の列へ入る(3 個目は 1 列目の下)
202        ("<div id='m'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
203         "#m{column-count:2} #a,#b,#c{height:10px}", 216, "c", (0, 10, 100, 10)),
204        // 33-d. columns ショートハンド(幅と列数の併記。count が上限)
205        ("<div id='m'><div id='a'></div><div id='b'></div></div>",
206         "#m{columns:100px 2;column-gap:0px} #a,#b{height:10px}", 200, "b", (100, 0, 100, 10)),
207        // 34. CSS Grid 1fr 2fr → 2列(210: col0=70, col1=140)
208        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
209         "#g{display:grid;grid-template-columns:1fr 2fr} #a,#b{height:10px}", 210, "b", (70, 0, 140, 10)),
210        // 35. CSS Grid repeat(auto-fit, minmax(80px,1fr)) → 200÷80=2列→各100px
211        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
212         "#g{display:grid;grid-template-columns:repeat(auto-fit,minmax(80px,1fr))} #a,#b{height:10px}", 200, "b", (100, 0, 100, 10)),
213        // 35b. スペース区切りの `%` トラック(repeat() を使わない形)→ `50% 1fr`。
214        // 以前は parse_px_like が `%` 単位に非対応で bare % トラックが 0px に丸め込まれる
215        // バグがあった(コンテナ200 → col0=100(50%), col1=残り100全部=1frで100)。
216        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
217         "#g{display:grid;grid-template-columns:50% 1fr} #a,#b{height:10px}", 200, "a", (0, 0, 100, 10)),
218        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
219         "#g{display:grid;grid-template-columns:50% 1fr} #a,#b{height:10px}", 200, "b", (100, 0, 100, 10)),
220        // 35c. repeat(auto-fit, minmax(%, 1fr)) の min 側が `%` の場合。以前は minmax の
221        // min 抽出が parse_px_like 直呼びで `%` 未対応のため常に既定値200pxへフォールバックし、
222        // auto-fit の列数計算が壊れていた。コンテナ200・min=50%(=100px) → floor(200/100)=2列、
223        // 各100px。
224        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
225         "#g{display:grid;grid-template-columns:repeat(auto-fit,minmax(50%,1fr))} #a,#b{height:10px}", 200, "b", (100, 0, 100, 10)),
226        // 36. aspect-ratio: 16/9 → width=160 なら height=90
227        ("<div id='t'></div>", "#t{width:160px;aspect-ratio:16/9}", 200, "t", (0, 0, 160, 90)),
228        // 37. aspect-ratio: 1/1 → 正方形
229        ("<div id='t'></div>", "#t{width:80px;aspect-ratio:1/1}", 200, "t", (0, 0, 80, 80)),
230        // 38. @media max-width: viewport=200 → ルール適用
231        ("<div id='t'></div>", "@media (max-width:300px){#t{height:42px}}", 200, "t", (0, 0, 200, 42)),
232        // 39. flex-direction:row-reverse → a が右端に配置(200-30=170)、b はその左(170-40=130)
233        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
234         "#f{display:flex;flex-direction:row-reverse} #a{width:30px;height:10px} #b{width:40px;height:10px}", 200, "a", (170, 0, 30, 10)),
235        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
236         "#f{display:flex;flex-direction:row-reverse} #a{width:30px;height:10px} #b{width:40px;height:10px}", 200, "b", (130, 0, 40, 10)),
237        // 40. align-self:flex-end → コンテナ高100、子高20 → y=80(align-items 既定 stretch を上書き)
238        ("<div id='f'><div id='a'></div></div>",
239         "#f{display:flex;height:100px} #a{width:30px;height:20px;align-self:flex-end}", 200, "a", (0, 80, 30, 20)),
240        // 41. align-self:center → コンテナ高100、子高20 → y=(100-20)/2=40
241        ("<div id='f'><div id='a'></div></div>",
242         "#f{display:flex;height:100px;align-items:flex-start} #a{width:30px;height:20px;align-self:center}", 200, "a", (0, 40, 30, 20)),
243        // 42. flex-wrap:wrap → 100px 幅3個がコンテナ200に2個入り、3個目が次行へ折り返す
244        ("<div id='f'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
245         "#f{display:flex;flex-wrap:wrap} #a{width:100px;height:10px} #b{width:100px;height:10px} #c{width:100px;height:10px}", 200, "c", (0, 10, 100, 10)),
246        // 43. flex-wrap:wrap → 2個目は1行目の x=100 に残る
247        ("<div id='f'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
248         "#f{display:flex;flex-wrap:wrap} #a{width:100px;height:10px} #b{width:100px;height:10px} #c{width:100px;height:10px}", 200, "b", (100, 0, 100, 10)),
249        // 44. flex 内 absolute: フレックスラインから除外され、flex を包含ブロックに top/left 配置。
250        //     フロー子 a は absolute を無視して x=0 のまま。
251        ("<div id='f'><div id='x'></div><div id='a'></div></div>",
252         "#f{display:flex;position:relative;height:100px} #x{position:absolute;top:10px;left:20px;width:30px;height:15px} #a{width:40px;height:10px}", 200, "x", (20, 10, 30, 15)),
253        // 45. flex 内 absolute はフロー外 → 兄弟 a は主軸先頭 x=0 を維持
254        ("<div id='f'><div id='x'></div><div id='a'></div></div>",
255         "#f{display:flex;position:relative;height:100px} #x{position:absolute;top:10px;left:20px;width:30px;height:15px} #a{width:40px;height:10px}", 200, "a", (0, 0, 40, 10)),
256        // 46. flex 内 relative: フロー位置(x=40)からオフセット(+5,+3)。a は width40。
257        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
258         "#f{display:flex} #a{width:40px;height:10px} #b{width:30px;height:10px;position:relative;left:5px;top:3px}", 200, "b", (45, 3, 30, 10)),
259        // 47. grid 内 absolute: グリッドセルを消費せず grid を包含ブロックに配置
260        ("<div id='g'><div id='x'></div><div id='a'></div><div id='b'></div></div>",
261         "#g{display:grid;position:relative;grid-template-columns:repeat(2,1fr)} #x{position:absolute;top:8px;left:12px;width:20px;height:10px} #a,#b{height:10px}", 200, "x", (12, 8, 20, 10)),
262        // 48. grid 内 absolute はセルを消費しない → a,b は col0/col1(各100)に残る
263        ("<div id='g'><div id='x'></div><div id='a'></div><div id='b'></div></div>",
264         "#g{display:grid;position:relative;grid-template-columns:repeat(2,1fr)} #x{position:absolute;top:8px;left:12px;width:20px;height:10px} #a,#b{height:10px}", 200, "b", (100, 0, 100, 10)),
265        // 49. table 内 relative セル: フロー配置(col1 起点 x=100, 自動レイアウトで幅100)後に
266        //     relative オフセット(left:7,top:4)を適用 → (107,4,100,10)
267        ("<table id='t'><tr><td id='c1'></td><td id='c2'></td></tr></table>",
268         "#t{position:relative} #c1{width:50px;height:10px} #c2{width:50px;height:10px;position:relative;left:7px;top:4px}", 200, "c2", (107, 4, 100, 10)),
269        // 50. grid-template-rows: 明示行高さ。2列2行、c は3番目=row1/col0、行0高さ20px → y=20
270        ("<div id='g'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
271         "#g{display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:20px 30px} #a,#b,#c{height:10px}", 200, "c", (0, 20, 100, 10)),
272        // 51. grid 明示配置: grid-column:2 で b を col1 へ(x=100)。grid-row 既定で row0
273        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
274         "#g{display:grid;grid-template-columns:repeat(2,1fr)} #a{height:10px} #b{height:10px;grid-column:2}", 200, "b", (100, 0, 100, 10)),
275        // 52. grid-row 明示: b を row2(=index1) へ。行高さは auto(10px)、b は y=10
276        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
277         "#g{display:grid;grid-template-columns:1fr} #a{height:10px} #b{height:10px;grid-row:2}", 200, "b", (0, 10, 200, 10)),
278        // 53. grid-column span: a が2列ぶん占有(幅 = 100+100 = 200)。3列各100px
279        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
280         "#g{display:grid;grid-template-columns:repeat(3,100px)} #a{height:10px;grid-column:span 2} #b{height:10px}", 400, "a", (0, 0, 200, 10)),
281        // 54. span 後の auto-flow: a が col0-1 を span、b は次の空き col2 へ(x=200)
282        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
283         "#g{display:grid;grid-template-columns:repeat(3,100px)} #a{height:10px;grid-column:span 2} #b{height:10px}", 400, "b", (200, 0, 100, 10)),
284        // 55. grid fr 行: コンテナ height:100、行 1fr 1fr → 各50。c(3番目)=row1 → y=50
285        ("<div id='g'><div id='a'></div><div id='b'></div><div id='c'></div><div id='d'></div></div>",
286         "#g{display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:1fr 1fr;height:100px} #a,#b,#c,#d{height:10px}", 200, "c", (0, 50, 100, 10)),
287        // --- flex-wrap align-content(2行: 各 60px 幅 → 100px 幅コンテナで折返し、行高 10、コンテナ高 100、余剰 80) ---
288        // 56. align-content:flex-start → 行を先頭に詰める。b は2行目 y=10
289        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
290         "#f{display:flex;flex-wrap:wrap;width:100px;height:100px;align-content:flex-start} #a{width:60px;height:10px} #b{width:60px;height:10px}", 200, "b", (0, 10, 60, 10)),
291        // 57. align-content:flex-end → 行ブロックを末尾へ。余剰80 → 1行目 y=80, b(2行目) y=90
292        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
293         "#f{display:flex;flex-wrap:wrap;width:100px;height:100px;align-content:flex-end} #a{width:60px;height:10px} #b{width:60px;height:10px}", 200, "b", (0, 90, 60, 10)),
294        // 58. align-content:center → 余剰80/2=40。a(1行目) y=40
295        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
296         "#f{display:flex;flex-wrap:wrap;width:100px;height:100px;align-content:center} #a{width:60px;height:10px} #b{width:60px;height:10px}", 200, "a", (0, 40, 60, 10)),
297        // 59. align-content:space-between → 1行目 y=0, 2行目 y=10+80=90。b → y=90
298        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
299         "#f{display:flex;flex-wrap:wrap;width:100px;height:100px;align-content:space-between} #a{width:60px;height:10px} #b{width:60px;height:10px}", 200, "b", (0, 90, 60, 10)),
300        // 60. align-content:stretch(既定)→ 各行高さ 10+40=50。b(2行目)は行頭 y=50
301        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
302         "#f{display:flex;flex-wrap:wrap;width:100px;height:100px;align-content:stretch} #a{width:60px;height:10px} #b{width:60px;height:10px}", 200, "b", (0, 50, 60, 10)),
303        // 61. align-content:space-around → ac_offset=80/4=20, 行間=40。b(2行目) y=20+10+40=70
304        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
305         "#f{display:flex;flex-wrap:wrap;width:100px;height:100px;align-content:space-around} #a{width:60px;height:10px} #b{width:60px;height:10px}", 200, "b", (0, 70, 60, 10)),
306        // --- grid アイテム整列(1列100px・明示サイズで余地を作る) ---
307        // 62. justify-self:center → セル幅100・item40 → dx=(100-40)/2=30
308        ("<div id='g'><div id='a'></div></div>",
309         "#g{display:grid;grid-template-columns:100px;width:200px} #a{width:40px;height:20px;justify-self:center}", 200, "a", (30, 0, 40, 20)),
310        // 63. justify-self:end → dx=100-40=60
311        ("<div id='g'><div id='a'></div></div>",
312         "#g{display:grid;grid-template-columns:100px;width:200px} #a{width:40px;height:20px;justify-self:end}", 200, "a", (60, 0, 40, 20)),
313        // 64. align-self:center → 行60・item20 → dy=(60-20)/2=20
314        ("<div id='g'><div id='a'></div></div>",
315         "#g{display:grid;grid-template-columns:100px;grid-template-rows:60px;width:200px} #a{width:40px;height:20px;align-self:center}", 200, "a", (0, 20, 40, 20)),
316        // 65. align-self:end → dy=60-20=40
317        ("<div id='g'><div id='a'></div></div>",
318         "#g{display:grid;grid-template-columns:100px;grid-template-rows:60px;width:200px} #a{width:40px;height:20px;align-self:end}", 200, "a", (0, 40, 40, 20)),
319        // 66. コンテナ justify-items:center(item 指定なし)→ dx=30
320        ("<div id='g'><div id='a'></div></div>",
321         "#g{display:grid;grid-template-columns:100px;width:200px;justify-items:center} #a{width:40px;height:20px}", 200, "a", (30, 0, 40, 20)),
322        // 67. コンテナ align-items:end(item 指定なし)→ 行60 → dy=40
323        ("<div id='g'><div id='a'></div></div>",
324         "#g{display:grid;grid-template-columns:100px;grid-template-rows:60px;width:200px;align-items:end} #a{width:40px;height:20px}", 200, "a", (0, 40, 40, 20)),
325        // 67b. グリッドコンテナ justify-content:center(トラック合計100 < コンテナ200
326        // で余剰100を左右に振り分け、以前は完全に未実装で常に左詰め固定だった)。
327        // 余剰100/2=50 → a の x=50。
328        ("<div id='g'><div id='a'></div></div>",
329         "#g{display:grid;grid-template-columns:100px;width:200px;justify-content:center} #a{width:100px;height:20px}", 200, "a", (50, 0, 100, 20)),
330        // 67c. justify-content:end → 余剰100を末尾へ寄せる → x=100。
331        ("<div id='g'><div id='a'></div></div>",
332         "#g{display:grid;grid-template-columns:100px;width:200px;justify-content:end} #a{width:100px;height:20px}", 200, "a", (100, 0, 100, 20)),
333        // 67d. justify-content:space-between(2列・各50px・余剰100を1箇所の
334        // トラック間隙に全振り)→ b(2列目) の x = 50 + 100 = 150。
335        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
336         "#g{display:grid;grid-template-columns:50px 50px;width:200px;justify-content:space-between} #a{width:50px;height:20px} #b{width:50px;height:20px}", 200, "b", (150, 0, 50, 20)),
337        // 67e. グリッドコンテナ align-content(行版 justify-content。明示 height
338        // がありトラック合計より大きい場合のみ余剰が発生する。以前はここも完全に
339        // 未実装で常に上詰め固定だった)。center: 1行50px・height:150 → 余剰100/2=50。
340        ("<div id='g'><div id='a'></div></div>",
341         "#g{display:grid;grid-template-rows:50px;height:150px;width:200px;align-content:center} #a{width:40px;height:50px}", 200, "a", (0, 50, 40, 50)),
342        // 67f. align-content:space-between(2行・各20px・height:150・余剰110を
343        // 1箇所の行間隙に全振り)→ b(2行目) の y = 20 + 110 = 130。
344        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
345         "#g{display:grid;grid-template-rows:20px 20px;height:150px;width:200px;align-content:space-between} #a{width:40px;height:20px} #b{width:40px;height:20px}", 200, "b", (0, 130, 40, 20)),
346        // --- flex-shrink / order ---
347        // 68. flex-shrink 既定(1): width80×2=160 がコンテナ100へ収まるよう超過60を均等に縮小 → 各50。a=50
348        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
349         "#f{display:flex;width:100px} #a{width:80px;height:10px} #b{width:80px;height:10px}", 200, "a", (0, 0, 50, 10)),
350        // 69. 同上で b は x=50, width50
351        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
352         "#f{display:flex;width:100px} #a{width:80px;height:10px} #b{width:80px;height:10px}", 200, "b", (50, 0, 50, 10)),
353        // 70. flex-shrink:0 の a は縮まず80、b(shrink既定1)が超過60を全部負担→20。b は x=80
354        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
355         "#f{display:flex;width:100px} #a{width:80px;height:10px;flex-shrink:0} #b{width:80px;height:10px}", 200, "b", (80, 0, 20, 10)),
356        // 71. order: c に order:-1 → c,a,b の順。b は最後尾 x=60(各30px)
357        ("<div id='f'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
358         "#f{display:flex} #a{width:30px;height:10px} #b{width:30px;height:10px} #c{width:30px;height:10px;order:-1}", 200, "b", (60, 0, 30, 10)),
359        // 72. order: 同上で c(order:-1)が先頭 x=0
360        ("<div id='f'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
361         "#f{display:flex} #a{width:30px;height:10px} #b{width:30px;height:10px} #c{width:30px;height:10px;order:-1}", 200, "c", (0, 0, 30, 10)),
362        // --- flex-basis(主軸初期サイズを width より優先) ---
363        // 73. flex-basis:60px が width:20px を上書き → a の幅60、b は x=60
364        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
365         "#f{display:flex} #a{width:20px;height:10px;flex-basis:60px} #b{width:30px;height:10px}", 200, "b", (60, 0, 30, 10)),
366        // 74. flex-basis:50% → コンテナ200の50%=100。a の幅100
367        ("<div id='f'><div id='a'></div></div>",
368         "#f{display:flex;width:200px} #a{height:10px;flex-basis:50%}", 200, "a", (0, 0, 100, 10)),
369        // 75. flex-basis + grow: a basis40 / b basis40, b に grow:1 → 余剰(200-80=120)を b へ → b 幅160, x=40
370        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
371         "#f{display:flex;width:200px} #a{height:10px;flex-basis:40px} #b{height:10px;flex-basis:40px;flex-grow:1}", 200, "b", (40, 0, 160, 10)),
372        // 75b. flex ショートハンドの basis に vw 単位 → `flex: 0 0 40vw`(viewport幅200の
373        // 40vw=80、grow=0/shrink=0で basis のまま)。以前は px/%/em のみ basis として
374        // 認識しておりvw/vh/remは非対応で basis が丸ごと失われるバグがあった
375        // (b の x offset で a の実際の幅を検証する)。
376        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
377         "#f{display:flex;width:200px} #a{height:10px;flex:0 0 40vw} #b{width:10px;height:10px}", 200, "b", (80, 0, 10, 10)),
378        // --- min-content / max-content / fit-content(inline-block 子で決定論的に検証) ---
379        // 76. width:max-content → 子 inline-block 50px×2 が横並び → 親幅100(1行 → 高さ10)
380        ("<div id='t'><span id='a'></span><span id='b'></span></div>",
381         "#t{width:max-content} #a{display:inline-block;width:50px;height:10px} #b{display:inline-block;width:50px;height:10px}", 400, "t", (0, 0, 100, 10)),
382        // 77. width:min-content → 最大の子幅 = 50。幅50だと 2 子が折返し2行 → 高さ20
383        ("<div id='t'><span id='a'></span><span id='b'></span></div>",
384         "#t{width:min-content} #a{display:inline-block;width:50px;height:10px} #b{display:inline-block;width:50px;height:10px}", 400, "t", (0, 0, 50, 20)),
385        // 78. width:fit-content → clamp(min=50, available=400, max=100)=100(1行 → 高さ10)
386        ("<div id='t'><span id='a'></span><span id='b'></span></div>",
387         "#t{width:fit-content} #a{display:inline-block;width:50px;height:10px} #b{display:inline-block;width:50px;height:10px}", 400, "t", (0, 0, 100, 10)),
388        // 79. fit-content が available でクランプ: available=70 < max=100 → 70(折返し2行 → 高さ20)
389        ("<div id='outer'><div id='t'><span id='a'></span><span id='b'></span></div></div>",
390         "#outer{width:70px} #t{width:fit-content} #a{display:inline-block;width:50px;height:10px} #b{display:inline-block;width:50px;height:10px}", 400, "t", (0, 0, 70, 20)),
391        // --- aspect-ratio: height 明示 + width:auto → width を逆算 ---
392        // 80. height:90 + aspect-ratio:16/9 → width = 90*16/9 = 160
393        ("<div id='t'></div>", "#t{height:90px;width:auto;aspect-ratio:16/9}", 400, "t", (0, 0, 160, 90)),
394        // 81. height:50 + aspect-ratio:2/1 → width = 100
395        ("<div id='t'></div>", "#t{height:50px;width:auto;aspect-ratio:2/1}", 400, "t", (0, 0, 100, 50)),
396        // 82. height:60 + aspect-ratio:1/1 → width=60。max-width:40 でクランプ → width=40
397        ("<div id='t'></div>", "#t{height:60px;width:auto;aspect-ratio:1/1;max-width:40px}", 400, "t", (0, 0, 40, 60)),
398        // --- flex 主軸 auto margin ---
399        // 83. margin-left:auto で b を右端へ押す。コンテナ200, a=30 b=40 → b は x=200-40=160
400        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
401         "#f{display:flex;width:200px} #a{width:30px;height:10px} #b{width:40px;height:10px;margin-left:auto}", 200, "b", (160, 0, 40, 10)),
402        // 84. margin:0 auto で単一アイテムを中央へ。余剰(200-50)=150 を左右 auto で半分ずつ → x=75
403        ("<div id='f'><div id='a'></div></div>",
404         "#f{display:flex;width:200px} #a{width:50px;height:10px;margin:0 auto}", 200, "a", (75, 0, 50, 10)),
405        // 85. column 方向 margin-top:auto で b を下端へ。コンテナ高100, a=10 b=20 → b は y=100-20=80
406        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
407         "#f{display:flex;flex-direction:column;height:100px} #a{width:10px;height:10px} #b{width:10px;height:20px;margin-top:auto}", 200, "b", (0, 80, 10, 20)),
408        // --- gap の % 対応(コンテナ content 幅基準) ---
409        // 86. flex row, column-gap:10% (200幅→20)。a=30 → b は x=30+20=50
410        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
411         "#f{display:flex;width:200px;column-gap:10%} #a{width:30px;height:10px} #b{width:40px;height:10px}", 200, "b", (50, 0, 40, 10)),
412        // 87. flex row, gap:25% (200→50)。a=20 → b は x=20+50=70
413        ("<div id='f'><div id='a'></div><div id='b'></div></div>",
414         "#f{display:flex;width:200px;gap:25%} #a{width:20px;height:10px} #b{width:20px;height:10px}", 200, "b", (70, 0, 20, 10)),
415        // 88. grid, column-gap:10% (200→20)、2列(repeat(2,80px))。b は col1 → x=80+20=100
416        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
417         "#g{display:grid;width:200px;grid-template-columns:repeat(2,80px);column-gap:10%} #a{height:10px} #b{height:10px}", 200, "b", (100, 0, 80, 10)),
418        // --- min-width / max-width への intrinsic キーワード ---
419        // 89. width:auto(=available400) だが max-width:min-content(=50) で 50 に制限
420        ("<div id='t'><span id='a'></span><span id='b'></span></div>",
421         "#t{max-width:min-content} #a{display:inline-block;width:50px;height:10px} #b{display:inline-block;width:50px;height:10px}", 400, "t", (0, 0, 50, 20)),
422        // 90. width:20px だが min-width:max-content(=100) で 100 へ拡張
423        ("<div id='t'><span id='a'></span><span id='b'></span></div>",
424         "#t{width:20px;min-width:max-content} #a{display:inline-block;width:50px;height:10px} #b{display:inline-block;width:50px;height:10px}", 400, "t", (0, 0, 100, 10)),
425        // --- grid minmax() 行トラック ---
426        // 91. minmax(50px,auto): 内容10px だが min 50 で行高50 → b(row1) は y=50
427        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
428         "#g{display:grid;grid-template-rows:minmax(50px,auto)} #a{height:10px} #b{height:10px}", 200, "b", (0, 50, 200, 10)),
429        // 92. minmax(10px,30px): 内容60px だが max 30 で行高30 → b(row1) は y=30
430        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
431         "#g{display:grid;grid-template-rows:minmax(10px,30px)} #a{height:60px} #b{height:10px}", 200, "b", (0, 30, 200, 10)),
432        // 93. minmax(20px,1fr) 1fr: コンテナ高100 → 各行 fr で50(>=20)→ b(row1) は y=50
433        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
434         "#g{display:grid;grid-template-rows:minmax(20px,1fr) 1fr;height:100px} #a{height:10px} #b{height:10px}", 200, "b", (0, 50, 200, 10)),
435        // 93b. grid-template-rows の `%` トラック(`50% 50%`)が明示 height 基準で解決される。
436        // 以前は行トラックの `%` 解決基準(base)が常に固定値0で渡されており、明示 height を
437        // 指定していても `%` トラックが常に0pxに丸め込まれるバグがあった(列側の同種バグは
438        // 既に修正済みで、行側だけ取り残されていた)。コンテナ高100 → 各行50px → b(row1) は y=50。
439        ("<div id='g'><div id='a'></div><div id='b'></div></div>",
440         "#g{display:grid;height:100px;grid-template-rows:50% 50%} #a{height:10px} #b{height:10px}", 200, "b", (0, 50, 200, 10)),
441        // --- grid-auto-rows(暗黙行のサイズ) ---
442        // 94. grid-template-rows なし、grid-auto-rows:40px、1列に3アイテム → c(row2) は y=80
443        ("<div id='g'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
444         "#g{display:grid;grid-template-columns:200px;grid-auto-rows:40px} #a{height:10px} #b{height:10px} #c{height:10px}", 200, "c", (0, 80, 200, 10)),
445        // 95. grid-auto-rows:30px 60px(循環): row0=30,row1=60,row2=30 → c(row2) は y=30+60=90
446        ("<div id='g'><div id='a'></div><div id='b'></div><div id='c'></div></div>",
447         "#g{display:grid;grid-template-columns:200px;grid-auto-rows:30px 60px} #a{height:10px} #b{height:10px} #c{height:10px}", 200, "c", (0, 90, 200, 10)),
448        // --- grid-auto-flow: column(列優先フロー) ---
449        // 96. 2列2行、column flow: a=(r0,c0) b=(r1,c0) c=(r0,c1) d=(r1,c1)。c は x=100,y=0
450        ("<div id='g'><div id='a'></div><div id='b'></div><div id='c'></div><div id='d'></div></div>",
451         "#g{display:grid;grid-template-columns:repeat(2,100px);grid-template-rows:50px 50px;grid-auto-flow:column} #a{height:10px} #b{height:10px} #c{height:10px} #d{height:10px}", 200, "c", (100, 0, 100, 10)),
452        // 97. 同上で b は2番目 → (r1,c0)。y=50
453        ("<div id='g'><div id='a'></div><div id='b'></div><div id='c'></div><div id='d'></div></div>",
454         "#g{display:grid;grid-template-columns:repeat(2,100px);grid-template-rows:50px 50px;grid-auto-flow:column} #a{height:10px} #b{height:10px} #c{height:10px} #d{height:10px}", 200, "b", (0, 50, 100, 10)),
455        // 97.5 grid-auto-columns(暗黙列): 明示2列(各100px)×2行=4セルを a,b,c,d が使い切り、
456        // 5番目の e は暗黙の3列目へあふれる。grid-auto-columns:80px によりその新しい列の
457        // 幅は80px、x = 100+100 = 200 になる(以前はここで無限ループしてハングしていた)。
458        ("<div id='g'><div id='a'></div><div id='b'></div><div id='c'></div><div id='d'></div><div id='e'></div></div>",
459         "#g{display:grid;grid-template-columns:repeat(2,100px);grid-template-rows:50px 50px;grid-auto-flow:column;grid-auto-columns:80px} #a{height:10px} #b{height:10px} #c{height:10px} #d{height:10px} #e{height:10px}", 300, "e", (200, 0, 80, 10)),
460        // --- float: left/right ---
461        // 98. float:left の a(幅50) は縦カーソルを進めず、b は a の隣(x=50)から始まる(floatの回避)
462        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
463         "#p{width:200px} #a{float:left;width:50px;height:30px} #b{width:100px;height:10px}", 200, "b", (50, 0, 100, 10)),
464        // 99. float:right の a(幅50) は右端に寄る。x=200-50=150
465        ("<div id='p'><div id='a'></div></div>",
466         "#p{width:200px} #a{float:right;width:50px;height:30px}", 200, "a", (150, 0, 50, 30)),
467        // 100. float:left の a(高さ30) の後、clear:left の c は a の底辺(y=30)まで下がる
468        ("<div id='p'><div id='a'></div><div id='c'></div></div>",
469         "#p{width:200px} #a{float:left;width:50px;height:30px} #c{clear:left;width:50px;height:10px}", 200, "c", (0, 30, 50, 10)),
470        // 101. float:left の a(高さ10) は非float兄弟(高さ20)の後でも親の高さに含まれる(コンテナ底辺=max(20,10)=20)
471        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
472         "#p{width:200px} #a{float:left;width:50px;height:10px} #b{width:100px;height:20px}", 200, "p", (0, 0, 200, 20)),
473        // --- float/clear の論理方向キーワード(inline-start/inline-end) ---
474        // 101b. direction 未指定(既定 ltr)では inline-start は float:left と同じ挙動。
475        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
476         "#p{width:200px} #a{float:inline-start;width:50px;height:30px} #b{width:100px;height:10px}", 200, "b", (50, 0, 100, 10)),
477        // 101c. direction:rtl では inline-start は float:right 相当(右端に寄る、x=200-50=150)。
478        ("<div id='p'><div id='a'></div></div>",
479         "#p{width:200px;direction:rtl} #a{float:inline-start;width:50px;height:30px}", 200, "a", (150, 0, 50, 30)),
480        // 101d. direction:rtl では inline-end は float:left 相当(bはaの隣、x=50から)。
481        ("<div id='p'><div id='a'></div><div id='b'></div></div>",
482         "#p{width:200px;direction:rtl} #a{float:inline-end;width:50px;height:30px} #b{width:100px;height:10px}", 200, "b", (50, 0, 100, 10)),
483        // --- grid-template-areas / grid-area ---
484        // 102. header が2列ぶんの named area を占有 → 幅200(col0+col1)、高さ=row0(20px)
485        ("<div id='g'><div id='h'></div><div id='s'></div><div id='c'></div></div>",
486         "#g{display:grid;grid-template-columns:100px 100px;grid-template-rows:20px 30px;grid-template-areas:'header header' 'sidebar content'} #h{grid-area:header} #s{grid-area:sidebar} #c{grid-area:content}",
487         200, "h", (0, 0, 200, 20)),
488        // 103. sidebar は row1,col0 → x=0,y=20(row0高さ),w=100,h=30(row1高さ)
489        ("<div id='g'><div id='h'></div><div id='s'></div><div id='c'></div></div>",
490         "#g{display:grid;grid-template-columns:100px 100px;grid-template-rows:20px 30px;grid-template-areas:'header header' 'sidebar content'} #h{grid-area:header} #s{grid-area:sidebar} #c{grid-area:content}",
491         200, "s", (0, 20, 100, 30)),
492        // 104. content は row1,col1 → x=100,y=20,w=100,h=30
493        ("<div id='g'><div id='h'></div><div id='s'></div><div id='c'></div></div>",
494         "#g{display:grid;grid-template-columns:100px 100px;grid-template-rows:20px 30px;grid-template-areas:'header header' 'sidebar content'} #h{grid-area:header} #s{grid-area:sidebar} #c{grid-area:content}",
495         200, "c", (100, 20, 100, 30)),
496        // --- calc() / min() / max() / clamp()(width への統一評価) ---
497        // 105. width:calc(100px + 50px) = 150
498        ("<div id='t'></div>", "#t{width:calc(100px + 50px);height:10px}", 400, "t", (0, 0, 150, 10)),
499        // 106. width:calc(50% - 20px)(コンテナ400 → 200-20=180)
500        ("<div id='t'></div>", "#t{width:calc(50% - 20px);height:10px}", 400, "t", (0, 0, 180, 10)),
501        // 107. width:min(300px, 50%)(コンテナ400 → 50%=200 < 300 → 200)
502        ("<div id='t'></div>", "#t{width:min(300px, 50%);height:10px}", 400, "t", (0, 0, 200, 10)),
503        // 108. width:max(300px, 50%)(コンテナ400 → 50%=200 < 300 → 300)
504        ("<div id='t'></div>", "#t{width:max(300px, 50%);height:10px}", 400, "t", (0, 0, 300, 10)),
505        // 109. width:clamp(100px, 50%, 300px)(コンテナ400 → 50%=200、100<=200<=300 → 200)
506        ("<div id='t'></div>", "#t{width:clamp(100px, 50%, 300px);height:10px}", 400, "t", (0, 0, 200, 10)),
507        // 110. width:clamp(100px, 10%, 300px)(コンテナ400 → 10%=40 < min100 → 100 にクランプ)
508        ("<div id='t'></div>", "#t{width:clamp(100px, 10%, 300px);height:10px}", 400, "t", (0, 0, 100, 10)),
509        // 111. width:clamp(100px, 90%, 300px)(コンテナ400 → 90%=360 > max300 → 300 にクランプ)
510        ("<div id='t'></div>", "#t{width:clamp(100px, 90%, 300px);height:10px}", 400, "t", (0, 0, 300, 10)),
511        // 112. margin-left:calc(10px * 3) = 30(b の x で確認)
512        // 通常の <div> はブロック要素で縦積みのため、b.x は a の幅とは無関係に margin-left の値そのもの。
513        ("<div id='a'></div><div id='b'></div>",
514         "#a{width:20px;height:10px} #b{width:20px;height:10px;margin-left:calc(10px * 3)}", 400, "b", (30, 10, 20, 10)),
515        // 113. table td vertical-align:middle — 行高さは隣接セル(height:100px)に合わせて伸びるが、
516        //      セル自身(td)の box は行頭に留まり、中の #t(height:10px)だけが余剰90pxの半分=45px下へ移動。
517        ("<table><tr><td id='a'></td><td class='c'><div id='t'></div></td></tr></table>",
518         "#a{width:50px;height:100px} .c{vertical-align:middle} #t{height:10px}", 400, "t", (200, 45, 200, 10)),
519        // 114. vertical-align:bottom — 余剰90px全部を移動。
520        ("<table><tr><td id='a'></td><td class='c'><div id='t'></div></td></tr></table>",
521         "#a{width:50px;height:100px} .c{vertical-align:bottom} #t{height:10px}", 400, "t", (200, 90, 200, 10)),
522        // 115. vertical-align 未指定(既定 top)— 移動なし。
523        ("<table><tr><td id='a'></td><td><div id='t'></div></td></tr></table>",
524         "#a{width:50px;height:100px} #t{height:10px}", 400, "t", (200, 0, 200, 10)),
525        // 116. td 自身の box は vertical-align に関わらず行頭に留まる(背景/境界がずれないことを確認)。
526        ("<table><tr><td id='a'></td><td id='t' class='c'></td></tr></table>",
527         "#a{width:50px;height:100px} .c{vertical-align:middle}", 400, "t", (200, 0, 200, 100)),
528        // 117. border-spacing:10px — 2列は利用可能幅(400-10)/2=195ずつに均等配分され、
529        //      2列目は195+10(spacing)=205から開始する。
530        ("<table id='tbl'><tr><td id='a'></td><td id='t'></td></tr></table>",
531         "#tbl{border-spacing:10px} #a{width:50px;height:20px} #t{width:50px;height:20px}", 400, "t", (205, 0, 195, 20)),
532        // 118. border-spacing の水平/垂直2値指定 + 2行 — 2行目は行高20+垂直spacing5=25から開始。
533        ("<table id='tbl'><tr><td id='a'></td></tr><tr><td id='t'></td></tr></table>",
534         "#tbl{border-spacing:10px 5px} #a{height:20px} #t{height:20px}", 400, "t", (0, 25, 400, 20)),
535        // 119. border-collapse:collapse では border-spacing は無効(隙間なし、1行目そのまま隣接)。
536        ("<table id='tbl'><tr><td id='a'></td><td id='t'></td></tr></table>",
537         "#tbl{border-collapse:collapse;border-spacing:10px} #a{width:50px;height:20px} #t{width:50px;height:20px}", 400, "t", (199, 0, 200, 20)),
538        // 119b. border-collapse は継承プロパティ。table 自身ではなく祖先(div)への指定が
539        // 伝播することを確認する(以前は INHERITED_PROPS に含まれておらず、table 自身への
540        // 直接指定でしか効かないバグがあった)。border-spacing は継承されないため table 自身へ
541        // 直接指定し、collapse だけが祖先経由で効いて隙間を無効化することを検証する。
542        ("<div id='wrap'><table id='tbl'><tr><td id='a'></td><td id='t'></td></tr></table></div>",
543         "#wrap{border-collapse:collapse} #tbl{border-spacing:10px} #a{width:50px;height:20px} #t{width:50px;height:20px}", 400, "t", (199, 0, 200, 20)),
544        // 120. caption-side 既定(top) — caption(高さ15)が先、行はその下 y=15。
545        ("<table><caption id='cap' style='height:15px'></caption><tr><td id='t' style='height:20px'></td></tr></table>",
546         "", 400, "t", (0, 15, 400, 20)),
547        // 121. caption-side:bottom — 行が先(y=0)、caption は行の下 y=20 に配置される。
548        ("<table><caption id='cap' style='height:15px'></caption><tr><td id='t' style='height:20px'></td></tr></table>",
549         "caption{caption-side:bottom}", 400, "t", (0, 0, 400, 20)),
550        // 122. 121と同じレイアウトで caption 自身の位置も確認(y=20)。
551        ("<table><caption id='cap' style='height:15px'></caption><tr><td id='t' style='height:20px'></td></tr></table>",
552         "caption{caption-side:bottom}", 400, "cap", (0, 20, 400, 15)),
553        // 123. direction:rtl かつ text-align 未指定 → 既定で右寄せ(offset=100-30=70)。
554        ("<div id='d'><span id='t'>x</span></div>",
555         "#d{direction:rtl;width:100px} #t{width:30px;height:10px}", 400, "t", (70, 0, 30, 10)),
556        // 124. direction:rtl でも text-align:left を明示すれば従来どおり左寄せ(調整なし)。
557        ("<div id='d'><span id='t'>x</span></div>",
558         "#d{direction:rtl;text-align:left;width:100px} #t{width:30px;height:10px}", 400, "t", (0, 0, 30, 10)),
559        // 125. table tr の visibility:collapse — 2行目(高さ20)が除去され、3行目(#t)は
560        //      1行目(高さ10)の直後 y=10 に詰めて配置される。
561        ("<table><tr><td style='height:10px'></td></tr><tr class='c'><td style='height:20px'></td></tr><tr><td id='t' style='height:15px'></td></tr></table>",
562         ".c{visibility:collapse}", 400, "t", (0, 10, 400, 15)),
563        // 126. table-layout:fixed — 1列目は最初の行の明示 width(100px)で確定、
564        //      2列目(auto)は残り利用可能幅(400-100=300)を受け取る。
565        ("<table id='tbl'><tr><td style='width:100px;height:20px'></td><td id='t' style='height:20px'></td></tr></table>",
566         "#tbl{table-layout:fixed}", 400, "t", (100, 0, 300, 20)),
567        // 127. table-layout:fixed — 列幅は最初の行のみで決まり、2行目セルの明示 width(200px)は
568        //      無視される(列は50pxのまま、内容/後続行の幅計測をしないのが仕様)。
569        ("<table id='tbl'><tr><td style='width:50px;height:10px'></td></tr><tr><td id='t' style='width:200px;height:20px'></td></tr></table>",
570         "#tbl{table-layout:fixed}", 400, "t", (0, 10, 50, 20)),
571        // 128. インライン(inline)コンテナ内の inline-block 子要素は既定で行頭(top)揃え。
572        ("<span id='wrap'><span id='a' style='display:inline-block;width:10px;height:30px'></span><span id='t' style='display:inline-block;width:10px;height:10px'></span></span>",
573         "", 400, "t", (10, 0, 10, 10)),
574        // 129. vertical-align:middle — 行の高さ(30, aの高さ)に対し中央 (30-10)/2=10 だけ下げる。
575        ("<span id='wrap'><span id='a' style='display:inline-block;width:10px;height:30px'></span><span id='t' style='display:inline-block;width:10px;height:10px;vertical-align:middle'></span></span>",
576         "", 400, "t", (10, 10, 10, 10)),
577        // 130. vertical-align:bottom — 行の下端に揃える (30-10=20)。
578        ("<span id='wrap'><span id='a' style='display:inline-block;width:10px;height:30px'></span><span id='t' style='display:inline-block;width:10px;height:10px;vertical-align:bottom'></span></span>",
579         "", 400, "t", (10, 20, 10, 10)),
580        // 131. vertical-align:middle でも基準となる 'a' 自身は行頭(top)のまま動かない。
581        ("<span id='wrap'><span id='a' style='display:inline-block;width:10px;height:30px'></span><span id='t' style='display:inline-block;width:10px;height:10px;vertical-align:middle'></span></span>",
582         "", 400, "a", (0, 0, 10, 30)),
583        // 132. text-align-last:right — 1行目('a', 250幅の唯一の行では無いため最終行ではない)は
584        //      text-align 無指定(left扱い)のままx=0。折り返した2行目('b', 最終行)だけ
585        //      text-align-last:right が適用され、offset=250-200=50 だけ右寄せされる。
586        ("<div id='c'><span id='a' style='display:inline-block;width:200px;height:10px'></span><span id='b' style='display:inline-block;width:200px;height:10px'></span></div>",
587         "#c{width:250px;text-align-last:right}", 400, "a", (0, 0, 200, 10)),
588        ("<div id='c'><span id='a' style='display:inline-block;width:200px;height:10px'></span><span id='b' style='display:inline-block;width:200px;height:10px'></span></div>",
589         "#c{width:250px;text-align-last:right}", 400, "b", (50, 10, 200, 10)),
590        // 133. text-align:end(論理方向キーワード)— 既定(ltr)では text-align:right と同じ
591        //      挙動。offset=100-30=70。
592        ("<div id='c'><span id='t' style='display:inline-block;width:30px;height:10px'></span></div>",
593         "#c{width:100px;text-align:end}", 400, "t", (70, 0, 30, 10)),
594        // 134. text-align:start + direction:rtl — rtl では start は right 相当。offset=70。
595        ("<div id='c'><span id='t' style='display:inline-block;width:30px;height:10px'></span></div>",
596         "#c{width:100px;direction:rtl;text-align:start}", 400, "t", (70, 0, 30, 10)),
597        // 135. text-align:end + direction:rtl — rtl では end は left 相当。offset=0(無調整)。
598        ("<div id='c'><span id='t' style='display:inline-block;width:30px;height:10px'></span></div>",
599         "#c{width:100px;direction:rtl;text-align:end}", 400, "t", (0, 0, 30, 10)),
600        // 136. HTML5 グローバル真偽属性 hidden — 著者スタイルで display が指定されていない
601        //      要素は hidden 属性により flow から除外される(display:none 相当)。
602        //      a(hidden) は除外され、b は y=0 に詰める。
603        ("<div id='a' hidden style='height:10px'></div><div id='b' style='height:10px'></div>",
604         "", 200, "b", (0, 0, 200, 10)),
605        // 137. hidden 属性があっても著者が明示的に display を指定していれば、そちらが優先される
606        //      (この実装の「著者スタイル優先」という簡略化方針に合わせる)。
607        ("<div id='a' hidden style='display:block;height:10px'></div><div id='b' style='height:10px'></div>",
608         "", 200, "b", (0, 10, 200, 10)),
609        // 138. HTML5 グローバル属性 dir="rtl" — CSS で direction を指定しなくても
610        //      属性から direction:rtl 相当が適用され、text-align 未指定時は既定で右寄せ
611        //      になる(テスト123の CSS direction:rtl 版と同じ期待値 offset=100-30=70)。
612        ("<div id='d' dir='rtl'><span id='t' style='width:30px;height:10px'></span></div>",
613         "#d{width:100px}", 400, "t", (70, 0, 30, 10)),
614        // 139. dir="rtl" 属性があっても、著者が CSS で direction を明示指定していれば
615        //      そちらが優先される(hidden と同じ「著者スタイル優先」方針)。
616        ("<div id='d' dir='rtl'><span id='t' style='width:30px;height:10px'></span></div>",
617         "#d{width:100px;direction:ltr}", 400, "t", (0, 0, 30, 10)),
618        // 139-1. text-align:justify — 1行目('a','b')は両端揃えで、gapが200-(100+10)=90px。
619        //        'a' は x=0, 'b' は x=100+90=190。
620        ("<div id='c'><span id='a' style='display:inline-block;width:100px;height:10px'></span><span id='b' style='display:inline-block;width:10px;height:10px'></span><span id='c2' style='display:inline-block;width:150px;height:10px'></span><span id='d' style='display:inline-block;width:50px;height:10px'></span></div>",
621         "#c{width:200px;text-align:justify}", 400, "a", (0, 0, 100, 10)),
622        ("<div id='c'><span id='a' style='display:inline-block;width:100px;height:10px'></span><span id='b' style='display:inline-block;width:10px;height:10px'></span><span id='c2' style='display:inline-block;width:150px;height:10px'></span><span id='d' style='display:inline-block;width:50px;height:10px'></span></div>",
623         "#c{width:200px;text-align:justify}", 400, "b", (190, 0, 10, 10)),
624        // 139-2. text-align:justify — 2行目(最終行 'c2','d')は左寄せ。
625        //        'c2' は x=0, 'd' は x=150。
626        ("<div id='c'><span id='a' style='display:inline-block;width:100px;height:10px'></span><span id='b' style='display:inline-block;width:10px;height:10px'></span><span id='c2' style='display:inline-block;width:150px;height:10px'></span><span id='d' style='display:inline-block;width:50px;height:10px'></span></div>",
627         "#c{width:200px;text-align:justify}", 400, "c2", (0, 10, 150, 10)),
628        ("<div id='c'><span id='a' style='display:inline-block;width:100px;height:10px'></span><span id='b' style='display:inline-block;width:10px;height:10px'></span><span id='c2' style='display:inline-block;width:150px;height:10px'></span><span id='d' style='display:inline-block;width:50px;height:10px'></span></div>",
629         "#c{width:200px;text-align:justify}", 400, "d", (150, 10, 50, 10)),
630        // 140. <template> の中身は文書のレンダーツリーに現れてはいけない(HTML5 仕様。
631        //      実際のコンテンツは DocumentFragment 側にあり、そこにいるだけでは描画されない)。
632        //      UA 既定 display:none により a は除外され、b は y=0 に詰める
633        //      (`<template>` 自体を DocumentFragment として特別扱いする完全実装ではなく、
634        //      「描画されない」という見た目上の効果だけを display:none で近似する簡略実装)。
635        ("<template id='a'><div style='height:10px'></div></template><div id='b' style='height:10px'></div>",
636         "", 200, "b", (0, 0, 200, 10)),
637        // 141. <noscript> — この処理系は常に JS を実行する(スクリプト有効)ため、
638        //      HTML5 仕様どおり常に非表示(display:none)にする。
639        ("<noscript>plain text fallback</noscript><div id='b' style='height:10px'></div>",
640         "", 200, "b", (0, 0, 200, 10)),
641        // 142-143. 既定 display が `_ => inline` に落ちてブロック要素として積み重ならない
642        //          バグを修正した各種 HTML5 セクショニング/フォーム要素。
643        //          <article> は block 既定なので b は a の直後 y=10 に積み重なる。
644        ("<article id='a' style='height:10px'></article><div id='b' style='height:10px'></div>",
645         "", 200, "b", (0, 10, 200, 10)),
646        // <figure>/<figcaption> も block 既定。
647        ("<figure id='a' style='height:10px'></figure><div id='b' style='height:10px'></div>",
648         "", 200, "b", (0, 10, 200, 10)),
649        // 144. <details> は open 属性が無い場合、最初の <summary> 以外の子要素が
650        //      display:none で flow から除外される(cascade.rs の details/summary
651        //      特別扱い。TODO.md「HTML5要素の<details>/<summary>」で「開閉に連動した
652        //      レイアウト分岐が未実装」と記載されていたが、実際には既に実装済みだった
653        //      ため、回帰防止のセルフテストとして新規追加)。
654        //      body id='c' は summary(高さ10)以外の本文が除外されるので summary 直後で
655        //      終わり、b は summary の高さ分(10)だけ下に積まれる。
656        ("<details id='d'><summary style='height:10px'></summary><div id='c' style='height:20px'></div></details><div id='b' style='height:10px'></div>",
657         "", 200, "b", (0, 10, 200, 10)),
658        // 145. <details open> では summary に続く本文 c も flow に含まれる
659        //      → b は summary(10) + c(20) = 30 の位置に積まれる。
660        ("<details id='d' open><summary style='height:10px'></summary><div id='c' style='height:20px'></div></details><div id='b' style='height:10px'></div>",
661         "", 200, "b", (0, 30, 200, 10)),
662        // 146. <details open> の本文 c 自体の矩形も正しく計算される(y=10、summary の直後)。
663        ("<details id='d' open><summary style='height:10px'></summary><div id='c' style='height:20px'></div></details>",
664         "", 200, "c", (0, 10, 200, 20)),
665        // 147. HTML5レガシー`width`/`height`属性(`<iframe width="300" height="150">`。
666        //      実サイト www.sugi-lab.net の Access ページの地図埋め込み
667        //      `<iframe width="100%" height="400">`と同じパターン)が、CSS指定が
668        //      無くてもcascade.rs側でwidth/heightへマッピングされ、サイズ未指定
669        //      要素として折りたたまれないことを検証(2026-07-21発見・修正)。
670        ("<iframe id='f' width='300' height='150'></iframe>",
671         "", 400, "f", (0, 0, 300, 150)),
672        // 148. `width="50%"`のような百分率指定も正しく解決される(親幅400pxの50% = 200px)。
673        ("<div id='p' style='width:400px'><iframe id='f' width='50%' height='100'></iframe></div>",
674         "", 400, "f", (0, 0, 200, 100)),
675        // 149. 著者CSSでwidth/heightが明示指定されていれば、レガシー属性より
676        //      そちらが優先される(「著者スタイル優先」の既存方針どおり)。
677        ("<iframe id='f' width='300' height='150' style='width:50px;height:20px'></iframe>",
678         "", 400, "f", (0, 0, 50, 20)),
679    ];
680    let mut passed = 0;
681    let total = cases.len();
682    for (html, css, vw, id, exp) in cases {
683        match measure(html, css, *vw, id) {
684            Some(d) if d == *exp => passed += 1,
685            Some(d) => crate::println!(
686                "LAYOUT_SELFTEST FAIL: #{} => ({},{},{},{}) want ({},{},{},{})",
687                id,
688                d.0,
689                d.1,
690                d.2,
691                d.3,
692                exp.0,
693                exp.1,
694                exp.2,
695                exp.3
696            ),
697            None => crate::println!("LAYOUT_SELFTEST FAIL: #{} NOT FOUND", id),
698        }
699    }
700
701    // --- インクリメンタル再レイアウト基盤の不変条件 ---
702    // WebEngine の CSS パースキャッシュ/HTML 再パース省略は、以下が成り立つ前提で安全:
703    //   (a) レイアウトは決定的(同じ入力 → 同じ出力)
704    //   (b) parse_css は決定的(同じ CSS 文字列 → 等価なスタイルシート)
705    //   (c) Node::empty_root() は子なしの html 要素(ブリッジ差し替えのプレースホルダ)
706    // これらをここで検証し、キャッシュ再利用の正当性を担保する。
707    let mut extra_passed = 0usize;
708    let mut extra_total = 0usize;
709
710    // (a)+(b) 同一 HTML/CSS を2回レイアウトして結果が完全一致すること(キャッシュ再利用の根拠)。
711    let det_html = "<div id='c'><div id='a'></div><div id='b'></div></div>";
712    let det_css = "#c{display:flex} #a{width:30px;height:10px} #b{width:40px;height:10px}";
713    extra_total += 1;
714    let r1 = measure(det_html, det_css, 200, "b");
715    let r2 = measure(det_html, det_css, 200, "b");
716    if r1.is_some() && r1 == r2 {
717        extra_passed += 1;
718    } else {
719        crate::println!("LAYOUT_SELFTEST FAIL: determinism r1={:?} r2={:?}", r1, r2);
720    }
721
722    // (b) キャッシュ済みスタイルシート(clone)を使っても、再パースと同じ結果になること。
723    extra_total += 1;
724    {
725        let dom = super::super::dom::parse_html(det_html);
726        let sheet_fresh = super::super::css::parse_css(det_css);
727        let sheet_cached = sheet_fresh.clone(); // WebEngine.cached_stylesheet 相当
728        set_viewport_hint(200, 600);
729        let icb = || Dimensions {
730            content: Rect {
731                x: 0,
732                y: 0,
733                width: 200,
734                height: 0,
735            },
736            ..Default::default()
737        };
738        let styled_a = super::super::css::style_tree(&dom, &sheet_fresh);
739        let styled_b = super::super::css::style_tree(&dom, &sheet_cached);
740        let ta = layout_tree(&styled_a, icb());
741        let tb = layout_tree(&styled_b, icb());
742        let da = find(&ta, "b").map(|b| {
743            let c = &b.dimensions.content;
744            (c.x, c.y, c.width, c.height)
745        });
746        let db = find(&tb, "b").map(|b| {
747            let c = &b.dimensions.content;
748            (c.x, c.y, c.width, c.height)
749        });
750        if da.is_some() && da == db {
751            extra_passed += 1;
752        } else {
753            crate::println!(
754                "LAYOUT_SELFTEST FAIL: css cache equiv da={:?} db={:?}",
755                da,
756                db
757            );
758        }
759    }
760
761    // (c) Node::empty_root() は子を持たない html 要素であること。
762    extra_total += 1;
763    {
764        let root = super::super::dom::Node::empty_root();
765        let is_html = matches!(
766            &root.node_type,
767            super::super::dom::NodeType::Element { tag_name, .. } if tag_name == "html"
768        );
769        if is_html && root.children.is_empty() {
770            extra_passed += 1;
771        } else {
772            crate::println!("LAYOUT_SELFTEST FAIL: empty_root shape");
773        }
774    }
775
776    // --- letter-spacing: 文字幅計測の不変条件(フォント非依存) ---
777    // get_vector_string_width_ls は base 幅に対して「文字数 × ls」だけ増減する。
778    // フォント実体に依存しない関係式で検証する(実機フォントが無い環境でも安定)。
779    {
780        use crate::kernel::vector_font::{get_vector_string_width, get_vector_string_width_ls};
781        let s = "ABCDE"; // 5 文字
782        let nchars = s.chars().count() as i32;
783        let base = get_vector_string_width(s, 16) as i32;
784
785        // ls=0 は base と一致
786        extra_total += 1;
787        if get_vector_string_width_ls(s, 16, 0) as i32 == base {
788            extra_passed += 1;
789        } else {
790            crate::println!("LAYOUT_SELFTEST FAIL: letter-spacing ls=0 != base");
791        }
792
793        // ls=2 → base + 5*2
794        extra_total += 1;
795        if get_vector_string_width_ls(s, 16, 2) as i32 == base + nchars * 2 {
796            extra_passed += 1;
797        } else {
798            crate::println!("LAYOUT_SELFTEST FAIL: letter-spacing ls=2 width");
799        }
800
801        // ls=-1 → base - 5(0 でクランプされない範囲)
802        extra_total += 1;
803        if get_vector_string_width_ls(s, 16, -1) as i32 == (base - nchars).max(0) {
804            extra_passed += 1;
805        } else {
806            crate::println!("LAYOUT_SELFTEST FAIL: letter-spacing ls=-1 width");
807        }
808
809        // parse_letter_spacing: normal/px/負値/未指定
810        extra_total += 1;
811        if parse_letter_spacing(Some("normal")) == 0
812            && parse_letter_spacing(Some("3px")) == 3
813            && parse_letter_spacing(Some("-2px")) == -2
814            && parse_letter_spacing(None) == 0
815            && parse_letter_spacing(Some("4")) == 4
816        {
817            extra_passed += 1;
818        } else {
819            crate::println!("LAYOUT_SELFTEST FAIL: parse_letter_spacing");
820        }
821
822        // parse_letter_spacing_ctx: calc()/min()/max()/clamp() にも対応する
823        // (以前は calc() 等が非対応で常に 0 に丸められていたバグの修正確認)。
824        extra_total += 1;
825        if parse_letter_spacing_ctx(Some("calc(1px + 1px)"), 0) == 2
826            && parse_letter_spacing_ctx(Some("min(3px, 5px)"), 0) == 3
827            && parse_letter_spacing_ctx(Some("max(3px, 5px)"), 0) == 5
828            && parse_letter_spacing_ctx(Some("clamp(1px, 10px, 5px)"), 0) == 5
829        {
830            extra_passed += 1;
831        } else {
832            crate::println!("LAYOUT_SELFTEST FAIL: parse_letter_spacing_ctx calc/min/max/clamp");
833        }
834    }
835
836    // --- word-spacing: 半角スペースの数 × ws だけ増減する不変条件(letter-spacing と同じ方式)。
837    {
838        use crate::kernel::vector_font::{get_vector_string_width, get_vector_string_width_ls_ws};
839        let s = "AB CD EF"; // 半角スペース2個
840        let base = get_vector_string_width(s, 16) as i32;
841
842        extra_total += 1;
843        if get_vector_string_width_ls_ws(s, 16, 0, 0) as i32 == base {
844            extra_passed += 1;
845        } else {
846            crate::println!("LAYOUT_SELFTEST FAIL: word-spacing ws=0 != base");
847        }
848
849        extra_total += 1;
850        if get_vector_string_width_ls_ws(s, 16, 0, 5) as i32 == base + 2 * 5 {
851            extra_passed += 1;
852        } else {
853            crate::println!("LAYOUT_SELFTEST FAIL: word-spacing ws=5 width");
854        }
855
856        extra_total += 1;
857        if parse_word_spacing(Some("normal")) == 0
858            && parse_word_spacing(Some("3px")) == 3
859            && parse_word_spacing(Some("-2px")) == -2
860            && parse_word_spacing(None) == 0
861        {
862            extra_passed += 1;
863        } else {
864            crate::println!("LAYOUT_SELFTEST FAIL: parse_word_spacing");
865        }
866    }
867
868    // `transform: translate()`の`resolve_translate_len`が`px`/`%`以外の単位
869    // (`rem`/`vh`/`vw`)を丸ごとサポートしていなかったバグ(`transform:
870    // translate(1rem, 10px)`のような式で意図した移動量が消えて要素が
871    // 動かなくなっていた。2026-07-17 発見・実装)。
872    {
873        // 他のテストケース(後続の`@media`ビューポート幅判定等)へ影響
874        // しないよう、変更前の値を退避し検証後に復元する(`css/
875        // selftest.rs`の`parse_px_like`検証で一度この復元漏れによる
876        // 回帰を経験済みの教訓を踏襲)。
877        let orig_w = VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed);
878        let orig_h = VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed);
879        set_viewport_hint(1000, 500);
880        let translate_cases: &[(&str, i32, i32)] = &[
881            ("10px", 0, 10),
882            ("50%", 200, 100),
883            ("2rem", 0, 32),
884            ("50vh", 0, 250),
885            ("50vw", 0, 500),
886        ];
887        for (tok, base, want) in translate_cases {
888            extra_total += 1;
889            let got = resolve_translate_len(tok, *base);
890            if got == *want {
891                extra_passed += 1;
892            } else {
893                crate::println!(
894                    "LAYOUT_SELFTEST FAIL: resolve_translate_len(`{}`, {}) => {} (want {})",
895                    tok, base, got, want
896                );
897            }
898        }
899        VIEWPORT_HINT_W.store(orig_w, AtomicOrdering::Relaxed);
900        VIEWPORT_HINT_H.store(orig_h, AtomicOrdering::Relaxed);
901    }
902
903    (passed + extra_passed, total + extra_total)
904}