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