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