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.
- package/FEATURES.md +102 -0
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/animator.d.ts +158 -0
- package/dist/animator.js +424 -0
- package/dist/animator.test.d.ts +5 -0
- package/dist/animator.test.js +169 -0
- package/dist/border-radius.test.d.ts +1 -0
- package/dist/border-radius.test.js +148 -0
- package/dist/capture.d.ts +193 -0
- package/dist/capture.js +786 -0
- package/dist/chrome.d.ts +45 -0
- package/dist/chrome.js +107 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +512 -0
- package/dist/client/dom.d.ts +10 -0
- package/dist/client/dom.js +17 -0
- package/dist/conic-raster.d.ts +58 -0
- package/dist/conic-raster.js +292 -0
- package/dist/conic-raster.test.d.ts +1 -0
- package/dist/conic-raster.test.js +187 -0
- package/dist/coretext-extractor.test.d.ts +1 -0
- package/dist/coretext-extractor.test.js +94 -0
- package/dist/coretext-helper.d.ts +60 -0
- package/dist/coretext-helper.js +205 -0
- package/dist/cross-origin-font-face.test.d.ts +1 -0
- package/dist/cross-origin-font-face.test.js +107 -0
- package/dist/cursor-overlay.d.ts +123 -0
- package/dist/cursor-overlay.js +207 -0
- package/dist/cursor-overlay.test.d.ts +1 -0
- package/dist/cursor-overlay.test.js +88 -0
- package/dist/dark-mode-capture.test.d.ts +1 -0
- package/dist/dark-mode-capture.test.js +158 -0
- package/dist/dark-mode-form-controls.test.d.ts +1 -0
- package/dist/dark-mode-form-controls.test.js +218 -0
- package/dist/dom-to-svg.d.ts +1016 -0
- package/dist/dom-to-svg.js +7717 -0
- package/dist/embed-remote-images.test.d.ts +1 -0
- package/dist/embed-remote-images.test.js +424 -0
- package/dist/form-controls.d.ts +70 -0
- package/dist/form-controls.js +1151 -0
- package/dist/frame-merge.d.ts +95 -0
- package/dist/frame-merge.js +374 -0
- package/dist/frame-merge.test.d.ts +6 -0
- package/dist/frame-merge.test.js +144 -0
- package/dist/gradients.d.ts +184 -0
- package/dist/gradients.js +937 -0
- package/dist/gradients.test.d.ts +1 -0
- package/dist/gradients.test.js +150 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +7 -0
- package/dist/jsx-runtime.d.ts +27 -0
- package/dist/jsx-runtime.js +96 -0
- package/dist/jsx-runtime.test.d.ts +1 -0
- package/dist/jsx-runtime.test.js +41 -0
- package/dist/kerfjs-imports.test.d.ts +1 -0
- package/dist/kerfjs-imports.test.js +36 -0
- package/dist/mask.test.d.ts +1 -0
- package/dist/mask.test.js +206 -0
- package/dist/optimize.d.ts +12 -0
- package/dist/optimize.js +32 -0
- package/dist/preserve-aspect-ratio.test.d.ts +1 -0
- package/dist/preserve-aspect-ratio.test.js +38 -0
- package/dist/resize-embedded-images.d.ts +33 -0
- package/dist/resize-embedded-images.js +164 -0
- package/dist/resize-embedded-images.test.d.ts +9 -0
- package/dist/resize-embedded-images.test.js +255 -0
- package/dist/stacking-context.test.d.ts +1 -0
- package/dist/stacking-context.test.js +927 -0
- package/dist/text-renderer.d.ts +42 -0
- package/dist/text-renderer.js +608 -0
- package/dist/text-renderer.test.d.ts +1 -0
- package/dist/text-renderer.test.js +150 -0
- package/dist/text-to-path.d.ts +265 -0
- package/dist/text-to-path.js +1800 -0
- package/dist/text-to-path.test.d.ts +1 -0
- package/dist/text-to-path.test.js +570 -0
- package/dist/utils/escapeHtml.d.ts +2 -0
- package/dist/utils/escapeHtml.js +15 -0
- package/dist/webfont-unicode-range.test.d.ts +1 -0
- package/dist/webfont-unicode-range.test.js +174 -0
- package/package.json +55 -0
- package/src/animator.test.ts +179 -0
- package/src/animator.ts +660 -0
- package/src/border-radius.test.ts +160 -0
- package/src/capture.ts +810 -0
- package/src/cli.ts +582 -0
- package/src/conic-raster.test.ts +213 -0
- package/src/conic-raster.ts +309 -0
- package/src/coretext-extractor.test.ts +130 -0
- package/src/coretext-helper.ts +256 -0
- package/src/cross-origin-font-face.test.ts +119 -0
- package/src/cursor-overlay.test.ts +95 -0
- package/src/cursor-overlay.ts +297 -0
- package/src/dark-mode-capture.test.ts +177 -0
- package/src/dark-mode-form-controls.test.ts +228 -0
- package/src/dom-to-svg.ts +8376 -0
- package/src/embed-remote-images.test.ts +461 -0
- package/src/form-controls.ts +1174 -0
- package/src/frame-merge.test.ts +157 -0
- package/src/frame-merge.ts +447 -0
- package/src/globals.d.ts +2 -0
- package/src/gradients.test.ts +175 -0
- package/src/gradients.ts +955 -0
- package/src/index.ts +12 -0
- package/src/kerf-jsx-augmentation.d.ts +36 -0
- package/src/kerfjs-imports.test.tsx +45 -0
- package/src/mask.test.ts +274 -0
- package/src/optimize.ts +34 -0
- package/src/preserve-aspect-ratio.test.ts +49 -0
- package/src/resize-embedded-images.test.ts +292 -0
- package/src/resize-embedded-images.ts +180 -0
- package/src/stacking-context.test.ts +967 -0
- package/src/text-renderer.test.ts +162 -0
- package/src/text-renderer.ts +623 -0
- package/src/text-to-path.test.ts +639 -0
- package/src/text-to-path.ts +1810 -0
- package/src/utils/escapeHtml.ts +16 -0
- 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
|
+
});
|