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