@flighthq/scene2d-formats 0.2.1-next.840.857425c

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.
@@ -0,0 +1,535 @@
1
+ import { createRectangle } from '@flighthq/geometry';
2
+ import { createImageResource } from '@flighthq/image';
3
+ import { getNodeChildAt, getNodeChildCount } from '@flighthq/node';
4
+ import { getShapeBounds } from '@flighthq/shape';
5
+ import type { Bitmap, ImportDiagnostic, RichText, Shape, TextLabel } from '@flighthq/types';
6
+ import { BitmapKind, DisplayObjectKind, RichTextKind, ShapeKind, TextLabelKind } from '@flighthq/types';
7
+
8
+ import { createScene2DFromSvgDocument } from './svgDocument';
9
+
10
+ describe('createScene2DFromSvgDocument', () => {
11
+ it('applies object-bounding-box clip and mask content units to target geometry', () => {
12
+ const root = createScene2DFromSvgDocument(`
13
+ <svg>
14
+ <defs>
15
+ <clipPath id="half" clipPathUnits="objectBoundingBox">
16
+ <rect width="0.5" height="1"/>
17
+ </clipPath>
18
+ <mask id="quarter" maskContentUnits="objectBoundingBox">
19
+ <rect x="0.25" width="0.5" height="1"/>
20
+ </mask>
21
+ </defs>
22
+ <rect width="200" height="100" clip-path="url(#half)"/>
23
+ <rect width="200" height="100" mask="url(#quarter)"/>
24
+ </svg>
25
+ `);
26
+
27
+ const clipped = getNodeChildAt(root, 0)!;
28
+ expect(clipped.clip?.rect).toMatchObject({ height: 100, width: 100, x: 0, y: 0 });
29
+ const masked = getNodeChildAt(root, 1)!;
30
+ expect(masked.clip?.rect).toMatchObject({ height: 100, width: 100, x: 50, y: 0 });
31
+ });
32
+
33
+ it('applies object-bounding-box clips in image-local geometry', () => {
34
+ const diagnostics: ImportDiagnostic[] = [];
35
+ const image = createImageResource();
36
+ image.width = 20;
37
+ image.height = 10;
38
+ const root = createScene2DFromSvgDocument(
39
+ `
40
+ <svg>
41
+ <defs>
42
+ <clipPath id="half" clipPathUnits="objectBoundingBox">
43
+ <rect width="0.5" height="1"/>
44
+ </clipPath>
45
+ <symbol id="panel" viewBox="0 0 20 10" preserveAspectRatio="none">
46
+ <rect width="20" height="10"/>
47
+ </symbol>
48
+ </defs>
49
+ <image href="asset.png" width="200" height="100" clip-path="url(#half)"/>
50
+ <g clip-path="url(#half)"><rect width="200" height="100"/></g>
51
+ <use href="#panel" width="200" height="100" clip-path="url(#half)"/>
52
+ <text font-size="10" clip-path="url(#half)">Unmeasured</text>
53
+ </svg>
54
+ `,
55
+ diagnostics,
56
+ { resolveImageResource: () => image },
57
+ );
58
+
59
+ const bitmap = getNodeChildAt(root, 0) as Bitmap;
60
+ expect(bitmap.clip?.rect).toMatchObject({ height: 10, width: 10, x: 0, y: 0 });
61
+ expect(bitmap.scaleX * (bitmap.clip?.rect.width ?? 0)).toBe(100);
62
+ expect(bitmap.scaleY * (bitmap.clip?.rect.height ?? 0)).toBe(100);
63
+ expect(getNodeChildAt(root, 1)?.clip?.rect).toMatchObject({ height: 100, width: 100, x: 0, y: 0 });
64
+ expect(getNodeChildAt(root, 2)?.clip?.rect).toMatchObject({ height: 100, width: 100, x: 0, y: 0 });
65
+ expect(getNodeChildAt(root, 3)?.clip).toBeNull();
66
+ expect(diagnostics).toContainEqual({
67
+ detail: { id: 'half' },
68
+ kind: 'svg.object-bounding-box-clip-without-bounds',
69
+ origin: 'applySvgElementClip',
70
+ severity: 'Skip',
71
+ });
72
+ });
73
+
74
+ it('applies root presentation to descendants', () => {
75
+ const root = createScene2DFromSvgDocument('<svg fill="red"><rect width="10" height="10"/></svg>');
76
+ const shape = getNodeChildAt(root, 0) as Shape;
77
+
78
+ expect(shape.data.commands).toContain(0xff0000);
79
+ });
80
+
81
+ it('composes image geometry before its SVG transform', () => {
82
+ const image = createImageResource();
83
+ image.width = 20;
84
+ image.height = 10;
85
+ const root = createScene2DFromSvgDocument(
86
+ '<svg><image href="asset.png" x="3" y="4" width="40" height="30" transform="translate(10 20)"/></svg>',
87
+ undefined,
88
+ { resolveImageResource: () => image },
89
+ );
90
+
91
+ const bitmap = getNodeChildAt(root, 0) as Bitmap;
92
+ expect(bitmap.x).toBe(13);
93
+ expect(bitmap.y).toBe(24);
94
+ expect(bitmap.scaleX).toBe(2);
95
+ expect(bitmap.scaleY).toBe(3);
96
+ });
97
+
98
+ it('composes authored geometry before transforms uniformly across element types', () => {
99
+ const image = createImageResource();
100
+ image.width = 10;
101
+ image.height = 10;
102
+ const root = createScene2DFromSvgDocument(
103
+ `
104
+ <svg>
105
+ <defs><g id="mark"><rect width="1" height="1"/></g></defs>
106
+ <rect x="3" width="10" height="10" transform="translate(10 5)"/>
107
+ <image href="asset.png" x="3" width="10" height="10" transform="translate(10 5)"/>
108
+ <use href="#mark" x="3" transform="translate(10 5)"/>
109
+ <text x="3" y="15" font-size="10" transform="translate(10 5)">T</text>
110
+ </svg>
111
+ `,
112
+ undefined,
113
+ { resolveImageResource: () => image },
114
+ );
115
+
116
+ const shape = getNodeChildAt(root, 0) as Shape;
117
+ const shapeBounds = createRectangle();
118
+ getShapeBounds(shapeBounds, shape);
119
+ expect(shape.x + shapeBounds.x).toBe(13);
120
+ expect((getNodeChildAt(root, 1) as Bitmap).x).toBe(13);
121
+ expect(getNodeChildAt(root, 2)?.x).toBe(13);
122
+ expect((getNodeChildAt(root, 3) as TextLabel).x).toBe(13);
123
+ });
124
+
125
+ it('composes use placement before its SVG transform', () => {
126
+ const root = createScene2DFromSvgDocument(`
127
+ <svg>
128
+ <defs><g id="mark"><rect width="1" height="1"/></g></defs>
129
+ <use href="#mark" x="7" y="9" transform="scale(2)"/>
130
+ </svg>
131
+ `);
132
+
133
+ const use = getNodeChildAt(root, 0)!;
134
+ expect(use.x).toBe(14);
135
+ expect(use.y).toBe(18);
136
+ expect(use.scaleX).toBe(2);
137
+ expect(use.scaleY).toBe(2);
138
+ });
139
+
140
+ it('diagnoses applied filters and nested animation at their use sites', () => {
141
+ const diagnostics: ImportDiagnostic[] = [];
142
+ createScene2DFromSvgDocument(
143
+ `
144
+ <svg>
145
+ <defs><filter id="blur"/></defs>
146
+ <rect width="10" height="10" filter="url(#blur)">
147
+ <animate attributeName="x" from="0" to="10"/>
148
+ </rect>
149
+ </svg>
150
+ `,
151
+ diagnostics,
152
+ );
153
+
154
+ expect(diagnostics.map((diagnostic) => diagnostic.kind)).toEqual([
155
+ 'svg.unsupported-filter',
156
+ 'svg.unsupported-animate',
157
+ ]);
158
+ });
159
+
160
+ it('inherits fill-rule through groups', () => {
161
+ const root = createScene2DFromSvgDocument(`
162
+ <svg><g fill-rule="evenodd"><path d="M0 0 L10 0 L10 10 Z"/></g></svg>
163
+ `);
164
+ const group = getNodeChildAt(root, 0)!;
165
+ const shape = getNodeChildAt(group, 0) as Shape;
166
+ const drawPathIndex = shape.data.commands.indexOf('drawPath');
167
+
168
+ expect(shape.data.commands[drawPathIndex + 4]).toBe('evenOdd');
169
+ });
170
+
171
+ it('honors clip-rule winding and diagnoses mixed clip child winding', () => {
172
+ const diagnostics: ImportDiagnostic[] = [];
173
+ const root = createScene2DFromSvgDocument(
174
+ `
175
+ <svg>
176
+ <defs>
177
+ <clipPath id="hole" clip-rule="evenodd">
178
+ <g><path d="M0 0 H20 V20 H0 Z M5 5 H15 V15 H5 Z"/></g>
179
+ </clipPath>
180
+ <clipPath id="mixed">
181
+ <rect width="10" height="10" clip-rule="evenodd"/>
182
+ <circle cx="5" cy="5" r="2" clip-rule="nonzero"/>
183
+ </clipPath>
184
+ <clipPath id="fillOnly" fill-rule="evenodd">
185
+ <path d="M0 0 H20 V20 H0 Z M5 5 H15 V15 H5 Z"/>
186
+ </clipPath>
187
+ </defs>
188
+ <rect width="20" height="20" clip-path="url(#hole)"/>
189
+ <rect width="20" height="20" clip-path="url(#mixed)"/>
190
+ <rect width="20" height="20" clip-path="url(#fillOnly)"/>
191
+ </svg>
192
+ `,
193
+ diagnostics,
194
+ );
195
+
196
+ expect(getNodeChildAt(root, 0)?.clip?.winding).toBe('evenOdd');
197
+ expect(getNodeChildAt(root, 2)?.clip?.winding).toBe('nonZero');
198
+ expect(diagnostics).toContainEqual({
199
+ detail: { id: 'mixed' },
200
+ kind: 'svg.mixed-clip-rule',
201
+ origin: 'createSvgClipPath',
202
+ severity: 'Recover',
203
+ });
204
+ });
205
+
206
+ it('instantiates use geometry inside clip paths with placement and transforms', () => {
207
+ const root = createScene2DFromSvgDocument(`
208
+ <svg>
209
+ <defs>
210
+ <path id="clipShape" d="M0 0 H10 V10 H0 Z"/>
211
+ <clipPath id="used"><use href="#clipShape" x="5" transform="scale(2)"/></clipPath>
212
+ </defs>
213
+ <rect width="40" height="40" clip-path="url(#used)"/>
214
+ </svg>
215
+ `);
216
+
217
+ expect(getNodeChildAt(root, 0)?.clip?.rect).toMatchObject({ height: 20, width: 20, x: 10, y: 0 });
218
+ });
219
+
220
+ it('honors display suppression and descendant visibility overrides in clip geometry', () => {
221
+ const root = createScene2DFromSvgDocument(`
222
+ <svg>
223
+ <defs>
224
+ <clipPath id="visibleOnly">
225
+ <rect x="0" width="10" height="10" display="none"/>
226
+ <rect x="10" width="10" height="10"/>
227
+ </clipPath>
228
+ <clipPath id="visibilityOverride">
229
+ <g visibility="hidden">
230
+ <rect x="0" width="10" height="10"/>
231
+ <rect x="20" width="10" height="10" visibility="visible"/>
232
+ </g>
233
+ </clipPath>
234
+ </defs>
235
+ <rect width="40" height="40" clip-path="url(#visibleOnly)"/>
236
+ <rect width="40" height="40" clip-path="url(#visibilityOverride)"/>
237
+ </svg>
238
+ `);
239
+
240
+ expect(getNodeChildAt(root, 0)?.clip?.rect).toMatchObject({ height: 10, width: 10, x: 10, y: 0 });
241
+ expect(getNodeChildAt(root, 1)?.clip?.rect).toMatchObject({ height: 10, width: 10, x: 20, y: 0 });
242
+ });
243
+
244
+ it('lets author CSS outrank presentation attributes by specificity and source order', () => {
245
+ const root = createScene2DFromSvgDocument(`
246
+ <svg>
247
+ <style>
248
+ rect { fill: red }
249
+ .accent { fill: green }
250
+ .accent { fill: orange }
251
+ </style>
252
+ <rect class="accent" fill="blue" width="10" height="10"/>
253
+ </svg>
254
+ `);
255
+ const shape = getNodeChildAt(root, 0) as Shape;
256
+
257
+ expect(shape.data.commands).toContain(0xffa500);
258
+ expect(shape.data.commands).not.toContain(0x0000ff);
259
+ });
260
+
261
+ it('preserves mixed text and tspan source order', () => {
262
+ const root = createScene2DFromSvgDocument('<svg><text font-size="12">A<tspan fill="red">B</tspan>C</text></svg>');
263
+ const text = getNodeChildAt(root, 0) as RichText;
264
+
265
+ expect(text.data.text).toBe('ABC');
266
+ expect(text.data.textFormatRanges).toHaveLength(1);
267
+ expect(text.data.textFormatRanges[0]).toMatchObject({ end: 2, start: 1 });
268
+ expect(text.data.textFormatRanges[0].format.color).toBe(0xff0000ff);
269
+ });
270
+
271
+ it('preserves meaningful collapsed whitespace across mixed text runs', () => {
272
+ const root = createScene2DFromSvgDocument(
273
+ '<svg><text font-size="12">Hello <tspan fill="red">world</tspan>!</text></svg>',
274
+ );
275
+ const text = getNodeChildAt(root, 0) as RichText;
276
+
277
+ expect(text.data.text).toBe('Hello world!');
278
+ expect(text.data.textFormatRanges[0]).toMatchObject({ end: 11, start: 6 });
279
+ });
280
+
281
+ it('returns an empty tree and a structured diagnostic for non-SVG input', () => {
282
+ const diagnostics: ImportDiagnostic[] = [];
283
+ const root = createScene2DFromSvgDocument('<html/>', diagnostics);
284
+
285
+ expect(root.kind).toBe(DisplayObjectKind);
286
+ expect(getNodeChildCount(root)).toBe(0);
287
+ expect(diagnostics).toEqual([
288
+ {
289
+ detail: undefined,
290
+ kind: 'svg.invalid-document',
291
+ origin: 'createScene2DFromSvgDocument',
292
+ severity: 'Reject',
293
+ },
294
+ ]);
295
+ });
296
+
297
+ it('imports grouped geometry with inherited CSS presentation and transforms', () => {
298
+ const root = createScene2DFromSvgDocument(`
299
+ <svg id="art" width="200" height="100" viewBox="0 0 100 50">
300
+ <style>.accent { fill: #123456; stroke: rgb(255, 0, 0); stroke-width: 3 }</style>
301
+ <g id="layer" opacity="0.5" transform="translate(4 5)">
302
+ <path id="mark" class="accent" d="M0 0 L20 0 L20 10 Z"/>
303
+ </g>
304
+ </svg>
305
+ `);
306
+
307
+ expect(root.name).toBe('art');
308
+ expect(root.scaleX).toBe(2);
309
+ expect(root.scaleY).toBe(2);
310
+ const group = getNodeChildAt(root, 0)!;
311
+ expect(group.name).toBe('layer');
312
+ expect(group.alpha).toBe(0.5);
313
+ expect(group.x).toBe(4);
314
+ expect(group.y).toBe(5);
315
+ const shape = getNodeChildAt(group, 0) as Shape;
316
+ expect(shape.kind).toBe(ShapeKind);
317
+ expect(shape.name).toBe('mark');
318
+ expect(shape.data.commands).toContain('beginFill');
319
+ expect(shape.data.commands).toContain('lineStyle');
320
+ expect(shape.data.commands).toContain('drawPath');
321
+ expect(shape.data.commands).toContain(0x123456);
322
+ expect(shape.data.commands).toContain(0xff0000);
323
+ });
324
+
325
+ it('imports gradients, use references, text, clips, and mask degradation', () => {
326
+ const diagnostics: ImportDiagnostic[] = [];
327
+ const root = createScene2DFromSvgDocument(
328
+ `
329
+ <svg>
330
+ <defs>
331
+ <linearGradient id="paint" x1="0" y1="0" x2="80" y2="0">
332
+ <stop offset="0%" stop-color="#ff0000"/>
333
+ <stop offset="100%" style="stop-color:#0000ff;stop-opacity:.5"/>
334
+ </linearGradient>
335
+ <clipPath id="crop"><circle cx="10" cy="10" r="8"/></clipPath>
336
+ <mask id="matte"><rect x="0" y="0" width="20" height="20"/></mask>
337
+ <symbol id="badge"><rect width="10" height="6" fill="url(#paint)"/></symbol>
338
+ </defs>
339
+ <use id="first" href="#badge" x="7" y="9" clip-path="url(#crop)"/>
340
+ <text id="caption" x="12" y="30" font-family="Inter" font-size="14" fill="#336699">Hello</text>
341
+ <rect id="masked" width="20" height="20" mask="url(#matte)"/>
342
+ </svg>
343
+ `,
344
+ diagnostics,
345
+ );
346
+
347
+ const use = getNodeChildAt(root, 0)!;
348
+ expect(use.name).toBe('first');
349
+ expect(use.x).toBe(7);
350
+ expect(use.y).toBe(9);
351
+ expect(use.clip?.contours).not.toBeNull();
352
+ const symbol = getNodeChildAt(use, 0)!;
353
+ const gradientShape = getNodeChildAt(symbol, 0) as Shape;
354
+ const gradientCommandIndex = gradientShape.data.commands.indexOf('beginGradientFill');
355
+ expect(gradientCommandIndex).toBeGreaterThanOrEqual(0);
356
+ expect(gradientShape.data.commands[gradientCommandIndex + 3]).toEqual([0xff0000, 0x0000ff]);
357
+
358
+ const text = getNodeChildAt(root, 1) as TextLabel;
359
+ expect(text.kind).toBe(TextLabelKind);
360
+ expect(text.name).toBe('caption');
361
+ expect(text.data.text).toBe('Hello');
362
+ expect(text.data.textFormat.font).toBe('Inter');
363
+ expect(text.data.textFormat.size).toBe(14);
364
+ expect(text.x).toBe(12);
365
+ expect(text.y).toBe(16);
366
+
367
+ const masked = getNodeChildAt(root, 2)!;
368
+ expect(masked.clip).not.toBeNull();
369
+ expect(diagnostics).toContainEqual({
370
+ detail: { id: 'matte' },
371
+ kind: 'svg.mask-as-hard-clip',
372
+ origin: 'applySvgElementClip',
373
+ severity: 'Recover',
374
+ });
375
+ });
376
+
377
+ it('imports every common SVG geometry element and skips live-document features loudly', () => {
378
+ const diagnostics: ImportDiagnostic[] = [];
379
+ const root = createScene2DFromSvgDocument(
380
+ `
381
+ <svg>
382
+ <rect x="1" y="2" width="3" height="4" rx="1"/>
383
+ <circle cx="5" cy="6" r="2"/>
384
+ <ellipse cx="8" cy="9" rx="3" ry="2"/>
385
+ <line x1="0" y1="0" x2="4" y2="5" stroke="black"/>
386
+ <polyline points="0,0 1,2 3,4" fill="none" stroke="blue"/>
387
+ <polygon points="0,0 5,0 2,4"/>
388
+ <image href="photo.png"/>
389
+ <foreignObject><div/></foreignObject>
390
+ </svg>
391
+ `,
392
+ diagnostics,
393
+ );
394
+
395
+ expect(getNodeChildCount(root)).toBe(6);
396
+ expect(diagnostics.map((diagnostic) => diagnostic.kind)).toEqual([
397
+ 'svg.unresolved-image',
398
+ 'svg.unsupported-foreignObject',
399
+ ]);
400
+ });
401
+
402
+ it('resolves image resources through an explicit no-I/O seam', () => {
403
+ const image = createImageResource();
404
+ image.width = 20;
405
+ image.height = 10;
406
+ const root = createScene2DFromSvgDocument(
407
+ '<svg><image id="photo" href="asset.png" x="3" y="4" width="40" height="30"/></svg>',
408
+ undefined,
409
+ { resolveImageResource: (href) => (href === 'asset.png' ? image : null) },
410
+ );
411
+
412
+ const bitmap = getNodeChildAt(root, 0) as Bitmap;
413
+ expect(bitmap.kind).toBe(BitmapKind);
414
+ expect(bitmap.name).toBe('photo');
415
+ expect(bitmap.data.image).toBe(image);
416
+ expect(bitmap.x).toBe(3);
417
+ expect(bitmap.y).toBe(4);
418
+ expect(bitmap.scaleX).toBe(2);
419
+ expect(bitmap.scaleY).toBe(3);
420
+ });
421
+
422
+ it('retains tspan style runs and diagnoses positional flattening', () => {
423
+ const diagnostics: ImportDiagnostic[] = [];
424
+ const root = createScene2DFromSvgDocument(
425
+ `
426
+ <svg>
427
+ <text id="mixed" font-family="Inter" font-size="12">
428
+ <tspan x="4" y="20" fill="#ff0000">Red</tspan>
429
+ <tspan x="30" y="20" fill="#0000ff" font-weight="bold">Blue</tspan>
430
+ </text>
431
+ </svg>
432
+ `,
433
+ diagnostics,
434
+ );
435
+
436
+ const text = getNodeChildAt(root, 0) as RichText;
437
+ expect(text.kind).toBe(RichTextKind);
438
+ expect(text.data.text).toBe('Red Blue');
439
+ expect(text.data.textFormatRanges).toHaveLength(2);
440
+ expect(text.data.textFormatRanges[0].format.color).toBe(0xff0000ff);
441
+ expect(text.data.textFormatRanges[1].format.color).toBe(0x0000ffff);
442
+ expect(text.data.textFormatRanges[1].format.bold).toBe(true);
443
+ expect(text.x).toBe(4);
444
+ expect(text.y).toBe(8);
445
+ expect(diagnostics).toContainEqual({
446
+ detail: { count: 2 },
447
+ kind: 'svg.tspan-position-flattened',
448
+ origin: 'createSvgTextNode',
449
+ severity: 'Recover',
450
+ });
451
+ });
452
+
453
+ it('composes text placement and baseline before its SVG transform', () => {
454
+ const root = createScene2DFromSvgDocument(
455
+ '<svg><text x="3" y="20" font-size="10" transform="translate(10 5)">Hello</text></svg>',
456
+ );
457
+ const text = getNodeChildAt(root, 0) as TextLabel;
458
+
459
+ expect(text.x).toBe(13);
460
+ expect(text.y).toBe(15);
461
+ });
462
+
463
+ it('honors preserveAspectRatio and forward gradient inheritance', () => {
464
+ const root = createScene2DFromSvgDocument(`
465
+ <svg width="200" height="100" viewBox="0 0 50 50">
466
+ <defs>
467
+ <linearGradient id="derived" href="#base" x2="100%"/>
468
+ <linearGradient id="base">
469
+ <stop offset="0" stop-color="red"/>
470
+ <stop offset="1" stop-color="blue"/>
471
+ </linearGradient>
472
+ </defs>
473
+ <rect width="50" height="50" fill="url(#derived)"/>
474
+ </svg>
475
+ `);
476
+
477
+ expect(root.scaleX).toBe(2);
478
+ expect(root.scaleY).toBe(2);
479
+ expect(root.x).toBe(50);
480
+ const shape = getNodeChildAt(root, 0) as Shape;
481
+ const gradientCommandIndex = shape.data.commands.indexOf('beginGradientFill');
482
+ expect(shape.data.commands[gradientCommandIndex + 3]).toEqual([0xff0000, 0x0000ff]);
483
+
484
+ const stretched = createScene2DFromSvgDocument(
485
+ '<svg width="200" height="100" viewBox="0 0 50 50" preserveAspectRatio="none"/>',
486
+ );
487
+ expect(stretched.scaleX).toBe(4);
488
+ expect(stretched.scaleY).toBe(2);
489
+ expect(stretched.x).toBe(0);
490
+ });
491
+
492
+ it('uses symbol viewBox semantics and the use viewport size', () => {
493
+ const root = createScene2DFromSvgDocument(`
494
+ <svg>
495
+ <defs>
496
+ <symbol id="icon" viewBox="0 0 10 10" preserveAspectRatio="none">
497
+ <rect width="10" height="10"/>
498
+ </symbol>
499
+ </defs>
500
+ <use href="#icon" width="20" height="30"/>
501
+ </svg>
502
+ `);
503
+
504
+ const use = getNodeChildAt(root, 0)!;
505
+ const symbol = getNodeChildAt(use, 0)!;
506
+ expect(symbol.scaleX).toBe(2);
507
+ expect(symbol.scaleY).toBe(3);
508
+ });
509
+
510
+ it('reports unsupported animation inside an instantiated symbol', () => {
511
+ const diagnostics: ImportDiagnostic[] = [];
512
+ createScene2DFromSvgDocument(
513
+ `
514
+ <svg>
515
+ <defs>
516
+ <symbol id="animated">
517
+ <rect width="10" height="10">
518
+ <animate attributeName="x" from="0" to="10"/>
519
+ </rect>
520
+ </symbol>
521
+ </defs>
522
+ <use href="#animated"/>
523
+ </svg>
524
+ `,
525
+ diagnostics,
526
+ );
527
+
528
+ expect(diagnostics).toContainEqual({
529
+ detail: { element: 'animate' },
530
+ kind: 'svg.unsupported-animate',
531
+ origin: 'createSvgElementNode',
532
+ severity: 'Skip',
533
+ });
534
+ });
535
+ });