domotion-svg 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/FEATURES.md +102 -0
  2. package/LICENSE +21 -0
  3. package/README.md +66 -0
  4. package/dist/animator.d.ts +158 -0
  5. package/dist/animator.js +424 -0
  6. package/dist/animator.test.d.ts +5 -0
  7. package/dist/animator.test.js +169 -0
  8. package/dist/border-radius.test.d.ts +1 -0
  9. package/dist/border-radius.test.js +148 -0
  10. package/dist/capture.d.ts +193 -0
  11. package/dist/capture.js +786 -0
  12. package/dist/chrome.d.ts +45 -0
  13. package/dist/chrome.js +107 -0
  14. package/dist/cli.d.ts +16 -0
  15. package/dist/cli.js +512 -0
  16. package/dist/client/dom.d.ts +10 -0
  17. package/dist/client/dom.js +17 -0
  18. package/dist/conic-raster.d.ts +58 -0
  19. package/dist/conic-raster.js +292 -0
  20. package/dist/conic-raster.test.d.ts +1 -0
  21. package/dist/conic-raster.test.js +187 -0
  22. package/dist/coretext-extractor.test.d.ts +1 -0
  23. package/dist/coretext-extractor.test.js +94 -0
  24. package/dist/coretext-helper.d.ts +60 -0
  25. package/dist/coretext-helper.js +205 -0
  26. package/dist/cross-origin-font-face.test.d.ts +1 -0
  27. package/dist/cross-origin-font-face.test.js +107 -0
  28. package/dist/cursor-overlay.d.ts +123 -0
  29. package/dist/cursor-overlay.js +207 -0
  30. package/dist/cursor-overlay.test.d.ts +1 -0
  31. package/dist/cursor-overlay.test.js +88 -0
  32. package/dist/dark-mode-capture.test.d.ts +1 -0
  33. package/dist/dark-mode-capture.test.js +158 -0
  34. package/dist/dark-mode-form-controls.test.d.ts +1 -0
  35. package/dist/dark-mode-form-controls.test.js +218 -0
  36. package/dist/dom-to-svg.d.ts +1016 -0
  37. package/dist/dom-to-svg.js +7717 -0
  38. package/dist/embed-remote-images.test.d.ts +1 -0
  39. package/dist/embed-remote-images.test.js +424 -0
  40. package/dist/form-controls.d.ts +70 -0
  41. package/dist/form-controls.js +1151 -0
  42. package/dist/frame-merge.d.ts +95 -0
  43. package/dist/frame-merge.js +374 -0
  44. package/dist/frame-merge.test.d.ts +6 -0
  45. package/dist/frame-merge.test.js +144 -0
  46. package/dist/gradients.d.ts +184 -0
  47. package/dist/gradients.js +937 -0
  48. package/dist/gradients.test.d.ts +1 -0
  49. package/dist/gradients.test.js +150 -0
  50. package/dist/index.d.ts +12 -0
  51. package/dist/index.js +7 -0
  52. package/dist/jsx-runtime.d.ts +27 -0
  53. package/dist/jsx-runtime.js +96 -0
  54. package/dist/jsx-runtime.test.d.ts +1 -0
  55. package/dist/jsx-runtime.test.js +41 -0
  56. package/dist/kerfjs-imports.test.d.ts +1 -0
  57. package/dist/kerfjs-imports.test.js +36 -0
  58. package/dist/mask.test.d.ts +1 -0
  59. package/dist/mask.test.js +206 -0
  60. package/dist/optimize.d.ts +12 -0
  61. package/dist/optimize.js +32 -0
  62. package/dist/preserve-aspect-ratio.test.d.ts +1 -0
  63. package/dist/preserve-aspect-ratio.test.js +38 -0
  64. package/dist/resize-embedded-images.d.ts +33 -0
  65. package/dist/resize-embedded-images.js +164 -0
  66. package/dist/resize-embedded-images.test.d.ts +9 -0
  67. package/dist/resize-embedded-images.test.js +255 -0
  68. package/dist/stacking-context.test.d.ts +1 -0
  69. package/dist/stacking-context.test.js +927 -0
  70. package/dist/text-renderer.d.ts +42 -0
  71. package/dist/text-renderer.js +608 -0
  72. package/dist/text-renderer.test.d.ts +1 -0
  73. package/dist/text-renderer.test.js +150 -0
  74. package/dist/text-to-path.d.ts +265 -0
  75. package/dist/text-to-path.js +1800 -0
  76. package/dist/text-to-path.test.d.ts +1 -0
  77. package/dist/text-to-path.test.js +570 -0
  78. package/dist/utils/escapeHtml.d.ts +2 -0
  79. package/dist/utils/escapeHtml.js +15 -0
  80. package/dist/webfont-unicode-range.test.d.ts +1 -0
  81. package/dist/webfont-unicode-range.test.js +174 -0
  82. package/package.json +55 -0
  83. package/src/animator.test.ts +179 -0
  84. package/src/animator.ts +660 -0
  85. package/src/border-radius.test.ts +160 -0
  86. package/src/capture.ts +810 -0
  87. package/src/cli.ts +582 -0
  88. package/src/conic-raster.test.ts +213 -0
  89. package/src/conic-raster.ts +309 -0
  90. package/src/coretext-extractor.test.ts +130 -0
  91. package/src/coretext-helper.ts +256 -0
  92. package/src/cross-origin-font-face.test.ts +119 -0
  93. package/src/cursor-overlay.test.ts +95 -0
  94. package/src/cursor-overlay.ts +297 -0
  95. package/src/dark-mode-capture.test.ts +177 -0
  96. package/src/dark-mode-form-controls.test.ts +228 -0
  97. package/src/dom-to-svg.ts +8376 -0
  98. package/src/embed-remote-images.test.ts +461 -0
  99. package/src/form-controls.ts +1174 -0
  100. package/src/frame-merge.test.ts +157 -0
  101. package/src/frame-merge.ts +447 -0
  102. package/src/globals.d.ts +2 -0
  103. package/src/gradients.test.ts +175 -0
  104. package/src/gradients.ts +955 -0
  105. package/src/index.ts +12 -0
  106. package/src/kerf-jsx-augmentation.d.ts +36 -0
  107. package/src/kerfjs-imports.test.tsx +45 -0
  108. package/src/mask.test.ts +274 -0
  109. package/src/optimize.ts +34 -0
  110. package/src/preserve-aspect-ratio.test.ts +49 -0
  111. package/src/resize-embedded-images.test.ts +292 -0
  112. package/src/resize-embedded-images.ts +180 -0
  113. package/src/stacking-context.test.ts +967 -0
  114. package/src/text-renderer.test.ts +162 -0
  115. package/src/text-renderer.ts +623 -0
  116. package/src/text-to-path.test.ts +639 -0
  117. package/src/text-to-path.ts +1810 -0
  118. package/src/utils/escapeHtml.ts +16 -0
  119. package/src/webfont-unicode-range.test.ts +207 -0
@@ -0,0 +1,927 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { elementTreeToSvg } from "./dom-to-svg.js";
3
+ /**
4
+ * DM-473: cross-stacking-context z-index unit tests.
5
+ *
6
+ * These exercise `establishesStackingContext` + `gatherStackingContextChildren`
7
+ * (both internal) by their observable effect on `elementTreeToSvg` output:
8
+ * the relative DOM order of `fill="rgb(...)"` rect emissions in the SVG
9
+ * string equals the paint order Chromium would use. The colours are unique
10
+ * per scenario so the fixture under test only needs to verify each colour
11
+ * appears in the expected sequence.
12
+ *
13
+ * Pairs with the integration fixtures in `tests/features.ts` (see the
14
+ * `z-index-cross-parent-non-context` / `z-index-stacking-context-boundary` /
15
+ * `z-index-negative-escapes` / `z-index-transform-stacking-context` blocks).
16
+ */
17
+ function makeElement(overrides = {}) {
18
+ return {
19
+ tag: "div",
20
+ text: "",
21
+ x: 0,
22
+ y: 0,
23
+ width: 100,
24
+ height: 100,
25
+ children: [],
26
+ ...overrides,
27
+ styles: {
28
+ backgroundColor: "rgb(255,255,255)",
29
+ borderColor: "rgb(0,0,0)",
30
+ borderWidth: "0",
31
+ borderRadius: "0",
32
+ borderTopLeftRadius: "0",
33
+ borderTopRightRadius: "0",
34
+ borderBottomRightRadius: "0",
35
+ borderBottomLeftRadius: "0",
36
+ borderTopWidth: "0",
37
+ borderRightWidth: "0",
38
+ borderBottomWidth: "0",
39
+ borderLeftWidth: "0",
40
+ borderTopColor: "rgb(0,0,0)",
41
+ borderRightColor: "rgb(0,0,0)",
42
+ borderBottomColor: "rgb(0,0,0)",
43
+ borderLeftColor: "rgb(0,0,0)",
44
+ borderTopStyle: "none",
45
+ borderRightStyle: "none",
46
+ borderBottomStyle: "none",
47
+ borderLeftStyle: "none",
48
+ color: "rgb(0,0,0)",
49
+ fontSize: "16px",
50
+ fontFamily: "sans-serif",
51
+ fontWeight: "400",
52
+ fontStyle: "normal",
53
+ lineHeight: "20px",
54
+ letterSpacing: "normal",
55
+ textAlign: "left",
56
+ textTransform: "none",
57
+ textDecoration: "none",
58
+ textDecorationLine: "none",
59
+ textDecorationStyle: "solid",
60
+ textDecorationColor: "rgb(0,0,0)",
61
+ textDecorationThickness: "auto",
62
+ textUnderlineOffset: "auto",
63
+ whiteSpace: "normal",
64
+ wordSpacing: "0",
65
+ verticalAlign: "baseline",
66
+ direction: "ltr",
67
+ writingMode: "horizontal-tb",
68
+ textOverflow: "clip",
69
+ cursor: "auto",
70
+ caretColor: "auto",
71
+ outlineColor: "rgb(0,0,0)",
72
+ outlineWidth: "0",
73
+ outlineStyle: "none",
74
+ outlineOffset: "0",
75
+ boxShadow: "none",
76
+ opacity: "1",
77
+ transform: "none",
78
+ transformOrigin: "50% 50%",
79
+ visibility: "visible",
80
+ borderCollapse: "separate",
81
+ overflowX: "visible",
82
+ overflowY: "visible",
83
+ scrollbarGutter: "auto",
84
+ scrollWidth: 100,
85
+ scrollHeight: 100,
86
+ clientWidth: 100,
87
+ clientHeight: 100,
88
+ scrollTop: 0,
89
+ scrollLeft: 0,
90
+ objectFit: "fill",
91
+ objectPosition: "50% 50%",
92
+ filter: "none",
93
+ backdropFilter: "none",
94
+ mixBlendMode: "normal",
95
+ clipPath: "none",
96
+ mask: "none",
97
+ maskImage: "none",
98
+ maskMode: "match-source",
99
+ maskSize: "auto",
100
+ maskPosition: "0% 0%",
101
+ maskRepeat: "repeat",
102
+ maskComposite: "add",
103
+ listStyleType: "disc",
104
+ listStyleImage: "none",
105
+ display: "block",
106
+ listStylePosition: "outside",
107
+ backgroundImage: "none",
108
+ backgroundSize: "auto",
109
+ backgroundPosition: "0% 0%",
110
+ backgroundRepeat: "repeat",
111
+ backgroundClip: "border-box",
112
+ backgroundOrigin: "padding-box",
113
+ backgroundAttachment: "scroll",
114
+ paddingTop: "0",
115
+ paddingRight: "0",
116
+ paddingBottom: "0",
117
+ paddingLeft: "0",
118
+ borderImageSource: "none",
119
+ borderImageSlice: "100%",
120
+ borderImageWidth: "1",
121
+ borderImageOutset: "0",
122
+ borderImageRepeat: "stretch",
123
+ zIndex: "auto",
124
+ position: "static",
125
+ float: "none",
126
+ order: "0",
127
+ flexDirection: "row",
128
+ ...(overrides.styles ?? {}),
129
+ },
130
+ };
131
+ }
132
+ /** Returns the order of `fill="<color>"` occurrences in the SVG. */
133
+ function fillOrder(svg, colors) {
134
+ const positions = colors
135
+ .map((c) => ({ c, i: svg.indexOf(`fill="${c}"`) }))
136
+ .filter((p) => p.i >= 0)
137
+ .sort((a, b) => a.i - b.i);
138
+ return positions.map((p) => p.c);
139
+ }
140
+ describe("DM-473 stacking-context paint order — cross-parent z-index", () => {
141
+ it("hoists positioned grandchild with z-index>0 above ancestor's later sibling when ancestor is non-SC", () => {
142
+ // Tree:
143
+ // root (relative)
144
+ // ├── red (absolute, z:auto, NOT SC) — bucket: zero/auto
145
+ // │ └── blue (absolute, z:1, SC) — hoists to root, bucket: positive(1)
146
+ // └── green (absolute, z:auto, NOT SC) — bucket: zero/auto, after red in DOM
147
+ //
148
+ // Paint order: bg → red → green → blue (blue hoists into positive(1) bucket
149
+ // so it paints AFTER both auto-bucket positioned siblings).
150
+ const tree = [makeElement({
151
+ x: 0, y: 0, width: 240, height: 160,
152
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
153
+ children: [
154
+ makeElement({
155
+ x: 10, y: 10, width: 120, height: 120,
156
+ styles: { ...makeElement().styles, position: "absolute", backgroundColor: "rgb(220,38,38)" },
157
+ children: [
158
+ makeElement({
159
+ x: 40, y: 40, width: 140, height: 90,
160
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "1", backgroundColor: "rgb(88,166,255)" },
161
+ }),
162
+ ],
163
+ }),
164
+ makeElement({
165
+ x: 80, y: 50, width: 140, height: 80,
166
+ styles: { ...makeElement().styles, position: "absolute", backgroundColor: "rgb(63,185,80)" },
167
+ }),
168
+ ],
169
+ })];
170
+ const svg = elementTreeToSvg(tree, 240, 160);
171
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(63,185,80)", "rgb(88,166,255)"]);
172
+ expect(order).toEqual([
173
+ "rgb(220,38,38)", // red painted first (z:auto, DOM order)
174
+ "rgb(63,185,80)", // green next (z:auto, after red)
175
+ "rgb(88,166,255)", // blue hoisted to positive(1) bucket — paints last
176
+ ]);
177
+ });
178
+ it("respects stacking-context boundary: z-index>5 inside SC doesn't escape to root", () => {
179
+ // Tree:
180
+ // root
181
+ // ├── red (absolute, z:1, IS SC) — bucket: positive(1)
182
+ // │ └── blue (absolute, z:5, SC) — STAYS inside red's SC
183
+ // └── green (absolute, z:2, IS SC) — bucket: positive(2), paints after red
184
+ //
185
+ // Paint order: red → blue (inside red) → green
186
+ const tree = [makeElement({
187
+ x: 0, y: 0, width: 240, height: 160,
188
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
189
+ children: [
190
+ makeElement({
191
+ x: 10, y: 10, width: 120, height: 120,
192
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "1", backgroundColor: "rgb(220,38,38)" },
193
+ children: [
194
+ makeElement({
195
+ x: 40, y: 40, width: 140, height: 90,
196
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "5", backgroundColor: "rgb(88,166,255)" },
197
+ }),
198
+ ],
199
+ }),
200
+ makeElement({
201
+ x: 80, y: 50, width: 140, height: 80,
202
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "2", backgroundColor: "rgb(63,185,80)" },
203
+ }),
204
+ ],
205
+ })];
206
+ const svg = elementTreeToSvg(tree, 240, 160);
207
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(88,166,255)", "rgb(63,185,80)"]);
208
+ expect(order).toEqual([
209
+ "rgb(220,38,38)", // red (z:1)
210
+ "rgb(88,166,255)", // blue inside red — DFS continues inside SC
211
+ "rgb(63,185,80)", // green (z:2) paints last
212
+ ]);
213
+ });
214
+ it("transform creates a stacking context — descendants don't escape", () => {
215
+ // Same shape as the boundary test but the SC root uses `transform`
216
+ // instead of an explicit z-index.
217
+ const tree = [makeElement({
218
+ x: 0, y: 0, width: 240, height: 160,
219
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
220
+ children: [
221
+ makeElement({
222
+ x: 10, y: 10, width: 120, height: 120,
223
+ styles: {
224
+ ...makeElement().styles,
225
+ position: "absolute",
226
+ transform: "translate(0px, 0px) matrix(1, 0, 0, 1, 0, 0)",
227
+ transformOrigin: "60px 60px",
228
+ backgroundColor: "rgb(220,38,38)",
229
+ },
230
+ children: [
231
+ makeElement({
232
+ x: 40, y: 40, width: 140, height: 90,
233
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "5", backgroundColor: "rgb(88,166,255)" },
234
+ }),
235
+ ],
236
+ }),
237
+ makeElement({
238
+ x: 80, y: 50, width: 140, height: 80,
239
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "1", backgroundColor: "rgb(63,185,80)" },
240
+ }),
241
+ ],
242
+ })];
243
+ const svg = elementTreeToSvg(tree, 240, 160);
244
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(88,166,255)", "rgb(63,185,80)"]);
245
+ expect(order).toEqual([
246
+ "rgb(220,38,38)", // red (transformed, SC root) — auto-z bucket at root
247
+ "rgb(88,166,255)", // blue inside red's SC
248
+ "rgb(63,185,80)", // green (z:1) paints last over both
249
+ ]);
250
+ });
251
+ it("negative z-index hoists below ancestor's earlier in-flow content", () => {
252
+ // Tree:
253
+ // root (relative)
254
+ // ├── gray (absolute, z:auto) — bucket: zero/auto
255
+ // └── red (absolute, z:auto, NOT SC) — bucket: zero/auto (after gray)
256
+ // └── blue (absolute, z:-1, SC) — hoists to root, bucket: NEGATIVE
257
+ //
258
+ // Paint order: bg → blue (negative bucket, first) → gray → red.
259
+ // Visually: blue paints behind both gray and red.
260
+ const tree = [makeElement({
261
+ x: 0, y: 0, width: 240, height: 160,
262
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
263
+ children: [
264
+ makeElement({
265
+ x: 10, y: 10, width: 220, height: 50,
266
+ styles: { ...makeElement().styles, position: "absolute", backgroundColor: "rgb(148,163,184)" },
267
+ }),
268
+ makeElement({
269
+ x: 60, y: 40, width: 120, height: 80,
270
+ styles: { ...makeElement().styles, position: "absolute", backgroundColor: "rgb(220,38,38)" },
271
+ children: [
272
+ makeElement({
273
+ x: 30, y: 20, width: 200, height: 120,
274
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "-1", backgroundColor: "rgb(88,166,255)" },
275
+ }),
276
+ ],
277
+ }),
278
+ ],
279
+ })];
280
+ const svg = elementTreeToSvg(tree, 240, 160);
281
+ const order = fillOrder(svg, ["rgb(88,166,255)", "rgb(148,163,184)", "rgb(220,38,38)"]);
282
+ expect(order).toEqual([
283
+ "rgb(88,166,255)", // blue (z:-1) hoisted to negative bucket — paints first
284
+ "rgb(148,163,184)", // gray (z:auto)
285
+ "rgb(220,38,38)", // red (z:auto, after gray in DOM)
286
+ ]);
287
+ });
288
+ it("will-change: transform creates a stacking context (DM-498)", () => {
289
+ // Apple-style hero pattern: artwork wrapper has `will-change: transform`
290
+ // (no explicit transform value yet — author intends to animate). Per CSS
291
+ // spec, this MUST create a stacking context. Without DM-498, Domotion
292
+ // missed this and positioned descendants escaped past the wrapper into
293
+ // the parent SC's flat list, disrupting paint order.
294
+ const tree = [makeElement({
295
+ x: 0, y: 0, width: 240, height: 160,
296
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
297
+ children: [
298
+ makeElement({
299
+ x: 10, y: 10, width: 120, height: 120,
300
+ styles: { ...makeElement().styles, position: "absolute", willChange: "transform", backgroundColor: "rgb(220,38,38)" },
301
+ children: [
302
+ makeElement({
303
+ x: 40, y: 40, width: 140, height: 90,
304
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "5", backgroundColor: "rgb(88,166,255)" },
305
+ }),
306
+ ],
307
+ }),
308
+ makeElement({
309
+ x: 80, y: 50, width: 140, height: 80,
310
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "1", backgroundColor: "rgb(63,185,80)" },
311
+ }),
312
+ ],
313
+ })];
314
+ const svg = elementTreeToSvg(tree, 240, 160);
315
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(88,166,255)", "rgb(63,185,80)"]);
316
+ expect(order).toEqual([
317
+ "rgb(220,38,38)", // will-change wrapper (auto-z bucket at root, paints first)
318
+ "rgb(88,166,255)", // blue trapped INSIDE the SC
319
+ "rgb(63,185,80)", // green (z:1) paints last via positive bucket
320
+ ]);
321
+ });
322
+ it("contain: paint creates a stacking context (DM-498)", () => {
323
+ // `contain: paint | strict | content` per CSS Containment spec creates
324
+ // a stacking context. Mirrors the will-change test — descendants stay
325
+ // inside.
326
+ const tree = [makeElement({
327
+ x: 0, y: 0, width: 240, height: 160,
328
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
329
+ children: [
330
+ makeElement({
331
+ x: 10, y: 10, width: 120, height: 120,
332
+ styles: { ...makeElement().styles, position: "absolute", contain: "paint", backgroundColor: "rgb(220,38,38)" },
333
+ children: [
334
+ makeElement({
335
+ x: 40, y: 40, width: 140, height: 90,
336
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "5", backgroundColor: "rgb(88,166,255)" },
337
+ }),
338
+ ],
339
+ }),
340
+ makeElement({
341
+ x: 80, y: 50, width: 140, height: 80,
342
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "1", backgroundColor: "rgb(63,185,80)" },
343
+ }),
344
+ ],
345
+ })];
346
+ const svg = elementTreeToSvg(tree, 240, 160);
347
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(88,166,255)", "rgb(63,185,80)"]);
348
+ expect(order).toEqual([
349
+ "rgb(220,38,38)",
350
+ "rgb(88,166,255)",
351
+ "rgb(63,185,80)",
352
+ ]);
353
+ });
354
+ it("isolation: isolate creates a stacking context (DM-498)", () => {
355
+ const tree = [makeElement({
356
+ x: 0, y: 0, width: 240, height: 160,
357
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
358
+ children: [
359
+ makeElement({
360
+ x: 10, y: 10, width: 120, height: 120,
361
+ styles: { ...makeElement().styles, position: "absolute", isolation: "isolate", backgroundColor: "rgb(220,38,38)" },
362
+ children: [
363
+ makeElement({
364
+ x: 40, y: 40, width: 140, height: 90,
365
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "5", backgroundColor: "rgb(88,166,255)" },
366
+ }),
367
+ ],
368
+ }),
369
+ makeElement({
370
+ x: 80, y: 50, width: 140, height: 80,
371
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "1", backgroundColor: "rgb(63,185,80)" },
372
+ }),
373
+ ],
374
+ })];
375
+ const svg = elementTreeToSvg(tree, 240, 160);
376
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(88,166,255)", "rgb(63,185,80)"]);
377
+ expect(order).toEqual([
378
+ "rgb(220,38,38)",
379
+ "rgb(88,166,255)",
380
+ "rgb(63,185,80)",
381
+ ]);
382
+ });
383
+ it("will-change: scroll-position does NOT create a stacking context", () => {
384
+ // CSS-Will-Change-1: only properties that themselves create SCs trigger
385
+ // SC formation when listed in will-change. `scroll-position` is not such
386
+ // a property — listing it should leave normal hoist behavior intact.
387
+ const tree = [makeElement({
388
+ x: 0, y: 0, width: 240, height: 160,
389
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
390
+ children: [
391
+ makeElement({
392
+ x: 10, y: 10, width: 120, height: 120,
393
+ styles: { ...makeElement().styles, position: "absolute", willChange: "scroll-position", backgroundColor: "rgb(220,38,38)" },
394
+ children: [
395
+ makeElement({
396
+ x: 40, y: 40, width: 140, height: 90,
397
+ // z-index:5, but parent is NOT an SC → blue hoists to root SC.
398
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "5", backgroundColor: "rgb(88,166,255)" },
399
+ }),
400
+ ],
401
+ }),
402
+ makeElement({
403
+ x: 80, y: 50, width: 140, height: 80,
404
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "1", backgroundColor: "rgb(63,185,80)" },
405
+ }),
406
+ ],
407
+ })];
408
+ const svg = elementTreeToSvg(tree, 240, 160);
409
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(88,166,255)", "rgb(63,185,80)"]);
410
+ // blue (z:5 hoisted to root) paints LAST — over green (z:1).
411
+ expect(order).toEqual([
412
+ "rgb(220,38,38)",
413
+ "rgb(63,185,80)",
414
+ "rgb(88,166,255)",
415
+ ]);
416
+ });
417
+ it("position:fixed/sticky always create a stacking context (modern CSS)", () => {
418
+ // A fixed-positioned ancestor with z-index:auto still creates an SC
419
+ // per the "modern CSS" rule. Its z-indexed descendants stay inside.
420
+ const tree = [makeElement({
421
+ x: 0, y: 0, width: 240, height: 160,
422
+ styles: { ...makeElement().styles, position: "relative", backgroundColor: "rgb(13,17,23)" },
423
+ children: [
424
+ makeElement({
425
+ x: 10, y: 10, width: 120, height: 120,
426
+ styles: { ...makeElement().styles, position: "fixed", backgroundColor: "rgb(220,38,38)" },
427
+ children: [
428
+ makeElement({
429
+ x: 40, y: 40, width: 140, height: 90,
430
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "5", backgroundColor: "rgb(88,166,255)" },
431
+ }),
432
+ ],
433
+ }),
434
+ makeElement({
435
+ x: 80, y: 50, width: 140, height: 80,
436
+ styles: { ...makeElement().styles, position: "absolute", zIndex: "1", backgroundColor: "rgb(63,185,80)" },
437
+ }),
438
+ ],
439
+ })];
440
+ const svg = elementTreeToSvg(tree, 240, 160);
441
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(88,166,255)", "rgb(63,185,80)"]);
442
+ expect(order).toEqual([
443
+ "rgb(220,38,38)", // fixed red (auto-z bucket at root)
444
+ "rgb(88,166,255)", // blue trapped inside red's SC
445
+ "rgb(63,185,80)", // green (z:1) paints last
446
+ ]);
447
+ });
448
+ });
449
+ describe("DM-525 flex/grid item z-index — stacking context without explicit positioning", () => {
450
+ // CSS Flexbox 1 §5.4 / CSS Grid 1: a flex/grid item with z-index ≠ auto
451
+ // creates a stacking context even when position:static, behaving as if
452
+ // position were relative. Without this, a static-positioned flex item
453
+ // with z-index:10 would NOT z-sort, and DOM order alone would decide
454
+ // overlapping paint — visible on `15-deep-flex-order-vs-z` (DM-525) where
455
+ // `<div class="item a" style="z-index: 10">A</div>` should pop above its
456
+ // overlapping siblings but Domotion painted it underneath.
457
+ it("paints a flex item with z-index:10 ABOVE its later DOM-order siblings (default position:static)", () => {
458
+ // Tree: a flex container with three static-positioned children.
459
+ // The first child has z-index:10 — per spec, it should paint LAST.
460
+ const tree = [makeElement({
461
+ x: 0, y: 0, width: 300, height: 100,
462
+ styles: { ...makeElement().styles, display: "flex", backgroundColor: "rgb(13,17,23)" },
463
+ children: [
464
+ makeElement({
465
+ x: 0, y: 0, width: 100, height: 100,
466
+ styles: { ...makeElement().styles, zIndex: "10", backgroundColor: "rgb(220,38,38)" }, // red, z:10
467
+ }),
468
+ makeElement({
469
+ x: 100, y: 0, width: 100, height: 100,
470
+ styles: { ...makeElement().styles, backgroundColor: "rgb(22,163,74)" }, // green, z:auto
471
+ }),
472
+ makeElement({
473
+ x: 200, y: 0, width: 100, height: 100,
474
+ styles: { ...makeElement().styles, backgroundColor: "rgb(37,99,235)" }, // blue, z:auto
475
+ }),
476
+ ],
477
+ })];
478
+ const svg = elementTreeToSvg(tree, 300, 100);
479
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(22,163,74)", "rgb(37,99,235)"]);
480
+ expect(order).toEqual([
481
+ "rgb(22,163,74)", // green (auto)
482
+ "rgb(37,99,235)", // blue (auto)
483
+ "rgb(220,38,38)", // red z:10 — paints LAST
484
+ ]);
485
+ });
486
+ it("z-sorts multiple flex items by their z-index, regardless of DOM order", () => {
487
+ const tree = [makeElement({
488
+ x: 0, y: 0, width: 300, height: 100,
489
+ styles: { ...makeElement().styles, display: "flex", backgroundColor: "rgb(13,17,23)" },
490
+ children: [
491
+ // a: z:5 — should paint last
492
+ makeElement({
493
+ x: 0, y: 0, width: 100, height: 100,
494
+ styles: { ...makeElement().styles, zIndex: "5", backgroundColor: "rgb(220,38,38)" },
495
+ }),
496
+ // b: z:1 — paints between auto-bucket and z:5
497
+ makeElement({
498
+ x: 100, y: 0, width: 100, height: 100,
499
+ styles: { ...makeElement().styles, zIndex: "1", backgroundColor: "rgb(22,163,74)" },
500
+ }),
501
+ // c: z:auto — paints first (before any explicit-z item per CSS)
502
+ makeElement({
503
+ x: 200, y: 0, width: 100, height: 100,
504
+ styles: { ...makeElement().styles, backgroundColor: "rgb(37,99,235)" },
505
+ }),
506
+ ],
507
+ })];
508
+ const svg = elementTreeToSvg(tree, 300, 100);
509
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(22,163,74)", "rgb(37,99,235)"]);
510
+ expect(order).toEqual([
511
+ "rgb(37,99,235)", // c: auto-bucket
512
+ "rgb(22,163,74)", // b: z:1
513
+ "rgb(220,38,38)", // a: z:5 — last
514
+ ]);
515
+ });
516
+ it("does NOT z-sort children of a non-flex/grid container with z-index (DOM order preserved)", () => {
517
+ // Sanity check: a regular block container ignores z-index on static
518
+ // children — they paint in DOM order.
519
+ const tree = [makeElement({
520
+ x: 0, y: 0, width: 300, height: 100,
521
+ styles: { ...makeElement().styles, display: "block", backgroundColor: "rgb(13,17,23)" },
522
+ children: [
523
+ makeElement({
524
+ x: 0, y: 0, width: 100, height: 100,
525
+ styles: { ...makeElement().styles, zIndex: "10", backgroundColor: "rgb(220,38,38)" }, // red, z:10 IGNORED
526
+ }),
527
+ makeElement({
528
+ x: 0, y: 0, width: 100, height: 100,
529
+ styles: { ...makeElement().styles, backgroundColor: "rgb(22,163,74)" }, // green
530
+ }),
531
+ ],
532
+ })];
533
+ const svg = elementTreeToSvg(tree, 300, 100);
534
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(22,163,74)"]);
535
+ expect(order).toEqual([
536
+ "rgb(220,38,38)", // DOM order: red first
537
+ "rgb(22,163,74)", // green second
538
+ ]);
539
+ });
540
+ it("treats grid items the same as flex items (display: grid)", () => {
541
+ const tree = [makeElement({
542
+ x: 0, y: 0, width: 300, height: 100,
543
+ styles: { ...makeElement().styles, display: "grid", backgroundColor: "rgb(13,17,23)" },
544
+ children: [
545
+ makeElement({
546
+ x: 0, y: 0, width: 100, height: 100,
547
+ styles: { ...makeElement().styles, zIndex: "10", backgroundColor: "rgb(220,38,38)" },
548
+ }),
549
+ makeElement({
550
+ x: 100, y: 0, width: 100, height: 100,
551
+ styles: { ...makeElement().styles, backgroundColor: "rgb(22,163,74)" },
552
+ }),
553
+ ],
554
+ })];
555
+ const svg = elementTreeToSvg(tree, 300, 100);
556
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(22,163,74)"]);
557
+ expect(order).toEqual([
558
+ "rgb(22,163,74)", // green (auto)
559
+ "rgb(220,38,38)", // red z:10 — last
560
+ ]);
561
+ });
562
+ });
563
+ describe("DM-558 flex/grid item z-index — buried-inside-non-SC-ancestor hoist", () => {
564
+ // CSS Flexbox 1 §5.4 / CSS Grid 1 §17 promote a flex/grid item with
565
+ // explicit z-index to a stacking-context root even at position:static.
566
+ // When such an item is BURIED inside a non-SC ancestor's sub-tree (the
567
+ // flex item's parent is itself a non-SC element that's a child of the
568
+ // real SC root), the item must hoist into the real SC root's paint sort
569
+ // — otherwise it stays nested at its DOM-order depth and paints BEFORE
570
+ // a sibling positioned element that should be beneath it.
571
+ //
572
+ // Real-world case: apple.com hero `tile-wrapper > tile-content > tile-ctas
573
+ // > a.button` (button is a grid item of `tile-ctas` with z:4). Without
574
+ // this hoist, the captured background image (sibling of `tile-content`
575
+ // under `tile-wrapper`) painted on top of the button.
576
+ function fillOrder(svg, expectedColors) {
577
+ const fillRe = /fill="(rgb\([^)]+\))"/g;
578
+ const found = [];
579
+ let m;
580
+ while ((m = fillRe.exec(svg)) !== null) {
581
+ if (expectedColors.includes(m[1]) && !found.includes(m[1])) {
582
+ found.push(m[1]);
583
+ }
584
+ }
585
+ return found;
586
+ }
587
+ it("hoists a buried flex/grid-item-with-z above its non-SC parent's later sibling", () => {
588
+ // Mirror apple's structure simplified:
589
+ // tile-wrapper (display:flex + overflow:clip → SC root)
590
+ // ├─ tile-content (position:relative, z:auto — non-SC) [DOM 0]
591
+ // │ └─ tile-ctas (display:grid)
592
+ // │ └─ a.button (position:static, z:4) — must paint LAST
593
+ // └─ tile-image-wrapper (position:absolute, z:auto — non-SC) [DOM 1]
594
+ // └─ image-bg (the bg that wrongly painted on top of button)
595
+ const tree = [makeElement({
596
+ x: 0, y: 0, width: 300, height: 100,
597
+ styles: {
598
+ ...makeElement().styles,
599
+ display: "flex",
600
+ overflowX: "clip",
601
+ overflowY: "clip",
602
+ backgroundColor: "rgb(13,17,23)",
603
+ },
604
+ children: [
605
+ // tile-content
606
+ makeElement({
607
+ x: 0, y: 0, width: 300, height: 100,
608
+ styles: { ...makeElement().styles, display: "flex", position: "relative", backgroundColor: "rgb(255,255,255)" },
609
+ children: [
610
+ // tile-ctas (display:grid)
611
+ makeElement({
612
+ x: 100, y: 30, width: 100, height: 40,
613
+ styles: { ...makeElement().styles, display: "grid", backgroundColor: "rgb(255,255,255)" },
614
+ children: [
615
+ // a.button (grid-item with z:4, position:static)
616
+ makeElement({
617
+ x: 100, y: 30, width: 100, height: 40,
618
+ styles: { ...makeElement().styles, zIndex: "4", backgroundColor: "rgb(0,113,227)" }, // blue button
619
+ }),
620
+ ],
621
+ }),
622
+ ],
623
+ }),
624
+ // tile-image-wrapper (positioned, covers same x range)
625
+ makeElement({
626
+ x: 0, y: 0, width: 300, height: 100,
627
+ styles: { ...makeElement().styles, position: "absolute", backgroundColor: "rgb(220,38,38)" }, // red image bg
628
+ }),
629
+ ],
630
+ })];
631
+ const svg = elementTreeToSvg(tree, 300, 100);
632
+ const order = fillOrder(svg, ["rgb(0,113,227)", "rgb(220,38,38)"]);
633
+ // tile-image-wrapper (red) must paint BEFORE the button (blue) so that
634
+ // the button is visible on top — matches Chromium for the real apple
635
+ // hero.
636
+ expect(order).toEqual([
637
+ "rgb(220,38,38)", // tile-image-wrapper
638
+ "rgb(0,113,227)", // a.button (hoisted past its non-SC ancestor)
639
+ ]);
640
+ });
641
+ });
642
+ describe("DM-537 flex/grid `order` property — paint follows order-modified document order", () => {
643
+ // CSS Flexbox 1 §5.4.1 / CSS Grid 1 §17: flex/grid items paint in
644
+ // order-modified document order (ascending `order`, ties broken by source
645
+ // order). The `order` property reorders both the visual layout AND the
646
+ // paint stack. This is the residual bug in `15-deep-flex-order-vs-z`
647
+ // section 2 after DM-525 fixed the explicit-z-index portion: items
648
+ // `order: 5,4,3,2,1` reverse the visual layout so E (order:1) is leftmost
649
+ // and A (order:5) is rightmost; with default z:auto Chrome paints E first
650
+ // and A last (visual L-to-R order). Domotion was painting in DOM order
651
+ // (A first, E last), which matched the source order rather than the
652
+ // order-modified one — visible as colored stripes at the box-overlap
653
+ // zones since the wrong sibling covered each overlap.
654
+ it("paints flex items in ascending `order` value (default z:auto)", () => {
655
+ // Tree: 5 flex items, A first in DOM with order:5 (visually rightmost)
656
+ // through E last in DOM with order:1 (visually leftmost). Paint must
657
+ // be E, D, C, B, A — visual L-to-R / order-modified.
658
+ const tree = [makeElement({
659
+ x: 0, y: 0, width: 600, height: 60,
660
+ styles: { ...makeElement().styles, display: "flex", backgroundColor: "rgb(13,17,23)" },
661
+ children: [
662
+ makeElement({ x: 480, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, order: "5", backgroundColor: "rgb(220,38,38)" } }), // A
663
+ makeElement({ x: 360, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, order: "4", backgroundColor: "rgb(22,163,74)" } }), // B
664
+ makeElement({ x: 240, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, order: "3", backgroundColor: "rgb(37,99,235)" } }), // C
665
+ makeElement({ x: 120, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, order: "2", backgroundColor: "rgb(234,88,12)" } }), // D
666
+ makeElement({ x: 0, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, order: "1", backgroundColor: "rgb(124,58,237)" } }), // E
667
+ ],
668
+ })];
669
+ const svg = elementTreeToSvg(tree, 600, 60);
670
+ const order = fillOrder(svg, [
671
+ "rgb(220,38,38)", "rgb(22,163,74)", "rgb(37,99,235)", "rgb(234,88,12)", "rgb(124,58,237)",
672
+ ]);
673
+ expect(order).toEqual([
674
+ "rgb(124,58,237)", // E (order:1) first
675
+ "rgb(234,88,12)", // D (order:2)
676
+ "rgb(37,99,235)", // C (order:3)
677
+ "rgb(22,163,74)", // B (order:4)
678
+ "rgb(220,38,38)", // A (order:5) last/top
679
+ ]);
680
+ });
681
+ it("breaks `order` ties with source (DOM) order", () => {
682
+ // Two items share order:0, two share order:1. Within each bucket the
683
+ // painted-first is the one earlier in DOM order.
684
+ const tree = [makeElement({
685
+ x: 0, y: 0, width: 400, height: 60,
686
+ styles: { ...makeElement().styles, display: "flex", backgroundColor: "rgb(13,17,23)" },
687
+ children: [
688
+ makeElement({ x: 0, y: 0, width: 100, height: 60, styles: { ...makeElement().styles, order: "1", backgroundColor: "rgb(220,38,38)" } }), // red, order:1, DOM 0
689
+ makeElement({ x: 100, y: 0, width: 100, height: 60, styles: { ...makeElement().styles, order: "0", backgroundColor: "rgb(22,163,74)" } }), // green, order:0, DOM 1
690
+ makeElement({ x: 200, y: 0, width: 100, height: 60, styles: { ...makeElement().styles, order: "1", backgroundColor: "rgb(37,99,235)" } }), // blue, order:1, DOM 2
691
+ makeElement({ x: 300, y: 0, width: 100, height: 60, styles: { ...makeElement().styles, order: "0", backgroundColor: "rgb(234,88,12)" } }), // orange, order:0, DOM 3
692
+ ],
693
+ })];
694
+ const svg = elementTreeToSvg(tree, 400, 60);
695
+ const order = fillOrder(svg, [
696
+ "rgb(220,38,38)", "rgb(22,163,74)", "rgb(37,99,235)", "rgb(234,88,12)",
697
+ ]);
698
+ expect(order).toEqual([
699
+ "rgb(22,163,74)", // green (order:0, DOM 1) — first
700
+ "rgb(234,88,12)", // orange (order:0, DOM 3)
701
+ "rgb(220,38,38)", // red (order:1, DOM 0)
702
+ "rgb(37,99,235)", // blue (order:1, DOM 2) — last
703
+ ]);
704
+ });
705
+ it("ignores `order` on children of a non-flex/non-grid container (DOM order preserved)", () => {
706
+ // Plain block container — `order` has no effect.
707
+ const tree = [makeElement({
708
+ x: 0, y: 0, width: 200, height: 60,
709
+ styles: { ...makeElement().styles, display: "block", backgroundColor: "rgb(13,17,23)" },
710
+ children: [
711
+ makeElement({ x: 0, y: 0, width: 100, height: 60, styles: { ...makeElement().styles, order: "5", backgroundColor: "rgb(220,38,38)" } }),
712
+ makeElement({ x: 100, y: 0, width: 100, height: 60, styles: { ...makeElement().styles, order: "1", backgroundColor: "rgb(22,163,74)" } }),
713
+ ],
714
+ })];
715
+ const svg = elementTreeToSvg(tree, 200, 60);
716
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(22,163,74)"]);
717
+ expect(order).toEqual([
718
+ "rgb(220,38,38)", // DOM order — `order` ignored
719
+ "rgb(22,163,74)",
720
+ ]);
721
+ });
722
+ it("reverses paint order for flex-direction:row-reverse (visually-rightmost paints last)", () => {
723
+ // Section 3 of `15-deep-flex-order-vs-z`: 5 items in DOM order A,B,C,D,E
724
+ // inside `flex-direction: row-reverse` with no `order` on items. Items
725
+ // pack from main-start (right) so visual L-to-R is E,D,C,B,A. Chrome
726
+ // empirically paints in REVERSE DOM order — E first, A last — which
727
+ // ends up matching visual L-to-R. Without this rule Domotion painted
728
+ // in source order (A first, E last) and the colored stripes at the
729
+ // box-overlap zones diff'd because the wrong sibling owned each
730
+ // overlap (DM-537 follow-up to DM-525).
731
+ const tree = [makeElement({
732
+ x: 0, y: 0, width: 600, height: 60,
733
+ styles: { ...makeElement().styles, display: "flex", flexDirection: "row-reverse", backgroundColor: "rgb(13,17,23)" },
734
+ children: [
735
+ makeElement({ x: 480, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, backgroundColor: "rgb(220,38,38)" } }), // A — DOM 0, visually rightmost
736
+ makeElement({ x: 360, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, backgroundColor: "rgb(22,163,74)" } }), // B
737
+ makeElement({ x: 240, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, backgroundColor: "rgb(37,99,235)" } }), // C
738
+ makeElement({ x: 120, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, backgroundColor: "rgb(234,88,12)" } }), // D
739
+ makeElement({ x: 0, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, backgroundColor: "rgb(124,58,237)" } }), // E — DOM 4, visually leftmost
740
+ ],
741
+ })];
742
+ const svg = elementTreeToSvg(tree, 600, 60);
743
+ const order = fillOrder(svg, [
744
+ "rgb(220,38,38)", "rgb(22,163,74)", "rgb(37,99,235)", "rgb(234,88,12)", "rgb(124,58,237)",
745
+ ]);
746
+ expect(order).toEqual([
747
+ "rgb(124,58,237)", // E (DOM-last) paints first under row-reverse
748
+ "rgb(234,88,12)", // D
749
+ "rgb(37,99,235)", // C
750
+ "rgb(22,163,74)", // B
751
+ "rgb(220,38,38)", // A (DOM-first, visually rightmost) paints last/top
752
+ ]);
753
+ });
754
+ it("does NOT reverse paint order for flex-direction:row (default — DOM order)", () => {
755
+ // Section 1 sanity: same items but `flex-direction: row` (default) with
756
+ // no `order` set → paint = source/DOM order, A first, E last.
757
+ const tree = [makeElement({
758
+ x: 0, y: 0, width: 600, height: 60,
759
+ styles: { ...makeElement().styles, display: "flex", flexDirection: "row", backgroundColor: "rgb(13,17,23)" },
760
+ children: [
761
+ makeElement({ x: 0, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, backgroundColor: "rgb(220,38,38)" } }),
762
+ makeElement({ x: 120, y: 0, width: 120, height: 60, styles: { ...makeElement().styles, backgroundColor: "rgb(124,58,237)" } }),
763
+ ],
764
+ })];
765
+ const svg = elementTreeToSvg(tree, 600, 60);
766
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(124,58,237)"]);
767
+ expect(order).toEqual(["rgb(220,38,38)", "rgb(124,58,237)"]);
768
+ });
769
+ it("combines `order` reordering with explicit z-index buckets (z-index wins)", () => {
770
+ // green has order:5 (visually last) but z:1 — should paint AFTER red
771
+ // (z:auto) regardless of order. Red is order:1 (visually first) z:auto.
772
+ const tree = [makeElement({
773
+ x: 0, y: 0, width: 200, height: 60,
774
+ styles: { ...makeElement().styles, display: "flex", backgroundColor: "rgb(13,17,23)" },
775
+ children: [
776
+ makeElement({ x: 100, y: 0, width: 100, height: 60, styles: { ...makeElement().styles, order: "5", zIndex: "1", backgroundColor: "rgb(22,163,74)" } }), // green
777
+ makeElement({ x: 0, y: 0, width: 100, height: 60, styles: { ...makeElement().styles, order: "1", backgroundColor: "rgb(220,38,38)" } }), // red
778
+ ],
779
+ })];
780
+ const svg = elementTreeToSvg(tree, 200, 60);
781
+ const order = fillOrder(svg, ["rgb(220,38,38)", "rgb(22,163,74)"]);
782
+ expect(order).toEqual([
783
+ "rgb(220,38,38)", // red (auto bucket — paints before any explicit-z item)
784
+ "rgb(22,163,74)", // green (z:1 — last)
785
+ ]);
786
+ });
787
+ });
788
+ describe("DM-543 position:fixed escapes ancestor overflow clips", () => {
789
+ // CSS painting order: position:fixed paints in the viewport stacking
790
+ // context and escapes ALL ancestor overflow clips — UNLESS an ancestor
791
+ // creates a containing block for fixed (transform / filter / will-change:
792
+ // <transform|filter|perspective> / contain: <paint|strict|content|layout>),
793
+ // in which case the descendant is anchored to that ancestor and respects
794
+ // its clipping.
795
+ // The renderer wraps an overflow-clipping ancestor's children in
796
+ // `<g clip-path="url(#ovN)">`. We verify "escaped" vs "trapped" by
797
+ // checking whether the pin's fill appears AFTER the closing </g> of the
798
+ // clip group (escaped) or before it (trapped).
799
+ function clipState(svg, pinFill) {
800
+ const pinIdx = svg.indexOf(`fill="${pinFill}"`);
801
+ if (pinIdx < 0)
802
+ return "missing";
803
+ // Walk all <g.../</g> tokens up to pinIdx, tracking the open-tag stack.
804
+ // The pin is "trapped" if any frame on the stack at pinIdx is a
805
+ // clip-path group; "escaped" otherwise.
806
+ const tokens = svg.slice(0, pinIdx).matchAll(/<g\b[^>]*>|<\/g>/g);
807
+ const stack = [];
808
+ for (const t of tokens) {
809
+ const s = t[0];
810
+ if (s === "</g>")
811
+ stack.pop();
812
+ else
813
+ stack.push(s);
814
+ }
815
+ return stack.some((s) => s.includes('clip-path="url(#ov')) ? "trapped" : "escaped";
816
+ }
817
+ it("hoists position:fixed inside an overflow:auto SC ancestor to the root SC", () => {
818
+ // Tree:
819
+ // section (overflow:auto SC, NOT a fixed CB)
820
+ // innerDiv (static, no CB)
821
+ // pin (position:fixed) ← should escape section's clip-path group
822
+ const tree = [makeElement({
823
+ x: 0, y: 0, width: 300, height: 100,
824
+ styles: { ...makeElement().styles, backgroundColor: "rgb(248,250,252)", overflowX: "auto", overflowY: "auto" },
825
+ children: [
826
+ makeElement({
827
+ x: 0, y: 0, width: 300, height: 200,
828
+ styles: { ...makeElement().styles, backgroundColor: "rgb(241,245,249)" },
829
+ children: [
830
+ makeElement({
831
+ x: 250, y: 80, width: 40, height: 16,
832
+ styles: { ...makeElement().styles, position: "fixed", backgroundColor: "rgb(220,38,38)" },
833
+ }),
834
+ ],
835
+ }),
836
+ ],
837
+ })];
838
+ const svg = elementTreeToSvg(tree, 300, 100);
839
+ expect(clipState(svg, "rgb(220,38,38)")).toBe("escaped");
840
+ });
841
+ it("traps position:fixed inside a transform-CB ancestor (does not over-escape)", () => {
842
+ // Tree:
843
+ // section (overflow:auto SC, NOT a fixed CB)
844
+ // frame (transform:translate(0) — IS a fixed CB)
845
+ // pin (position:fixed) ← stays trapped under frame inside section's clip
846
+ const tree = [makeElement({
847
+ x: 0, y: 0, width: 300, height: 100,
848
+ styles: { ...makeElement().styles, backgroundColor: "rgb(248,250,252)", overflowX: "auto", overflowY: "auto" },
849
+ children: [
850
+ makeElement({
851
+ x: 0, y: 0, width: 300, height: 200,
852
+ styles: { ...makeElement().styles, position: "relative", transform: "matrix(1, 0, 0, 1, 0, 0)", backgroundColor: "rgb(254,243,199)" },
853
+ children: [
854
+ makeElement({
855
+ x: 250, y: 180, width: 40, height: 16,
856
+ styles: { ...makeElement().styles, position: "fixed", backgroundColor: "rgb(220,38,38)" },
857
+ }),
858
+ ],
859
+ }),
860
+ ],
861
+ })];
862
+ const svg = elementTreeToSvg(tree, 300, 100);
863
+ expect(clipState(svg, "rgb(220,38,38)")).toBe("trapped");
864
+ });
865
+ it("traps position:fixed inside a contain:paint ancestor", () => {
866
+ const tree = [makeElement({
867
+ x: 0, y: 0, width: 300, height: 100,
868
+ styles: { ...makeElement().styles, backgroundColor: "rgb(248,250,252)", overflowX: "auto", overflowY: "auto" },
869
+ children: [
870
+ makeElement({
871
+ x: 0, y: 0, width: 300, height: 200,
872
+ styles: { ...makeElement().styles, position: "relative", contain: "paint", backgroundColor: "rgb(254,243,199)" },
873
+ children: [
874
+ makeElement({
875
+ x: 250, y: 180, width: 40, height: 16,
876
+ styles: { ...makeElement().styles, position: "fixed", backgroundColor: "rgb(220,38,38)" },
877
+ }),
878
+ ],
879
+ }),
880
+ ],
881
+ })];
882
+ const svg = elementTreeToSvg(tree, 300, 100);
883
+ expect(clipState(svg, "rgb(220,38,38)")).toBe("trapped");
884
+ });
885
+ it("traps position:fixed inside a will-change:transform ancestor", () => {
886
+ const tree = [makeElement({
887
+ x: 0, y: 0, width: 300, height: 100,
888
+ styles: { ...makeElement().styles, backgroundColor: "rgb(248,250,252)", overflowX: "auto", overflowY: "auto" },
889
+ children: [
890
+ makeElement({
891
+ x: 0, y: 0, width: 300, height: 200,
892
+ styles: { ...makeElement().styles, position: "relative", willChange: "transform", backgroundColor: "rgb(254,243,199)" },
893
+ children: [
894
+ makeElement({
895
+ x: 250, y: 180, width: 40, height: 16,
896
+ styles: { ...makeElement().styles, position: "fixed", backgroundColor: "rgb(220,38,38)" },
897
+ }),
898
+ ],
899
+ }),
900
+ ],
901
+ })];
902
+ const svg = elementTreeToSvg(tree, 300, 100);
903
+ expect(clipState(svg, "rgb(220,38,38)")).toBe("trapped");
904
+ });
905
+ it("hoists position:fixed past nested non-CB SC ancestors (overflow scroller inside overflow scroller)", () => {
906
+ // section1 (overflow:auto) > section2 (overflow:auto) > pin (fixed)
907
+ // Both scrollers are SCs but neither is a fixed-CB; pin escapes to root.
908
+ const tree = [makeElement({
909
+ x: 0, y: 0, width: 300, height: 100,
910
+ styles: { ...makeElement().styles, backgroundColor: "rgb(248,250,252)", overflowX: "auto", overflowY: "auto" },
911
+ children: [
912
+ makeElement({
913
+ x: 0, y: 0, width: 300, height: 200,
914
+ styles: { ...makeElement().styles, backgroundColor: "rgb(241,245,249)", overflowX: "auto", overflowY: "auto" },
915
+ children: [
916
+ makeElement({
917
+ x: 250, y: 80, width: 40, height: 16,
918
+ styles: { ...makeElement().styles, position: "fixed", backgroundColor: "rgb(220,38,38)" },
919
+ }),
920
+ ],
921
+ }),
922
+ ],
923
+ })];
924
+ const svg = elementTreeToSvg(tree, 300, 100);
925
+ expect(clipState(svg, "rgb(220,38,38)")).toBe("escaped");
926
+ });
927
+ });