@flighthq/displayobject-dom 0.1.0

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 (135) hide show
  1. package/dist/domBackground.d.ts +3 -0
  2. package/dist/domBackground.d.ts.map +1 -0
  3. package/dist/domBackground.js +9 -0
  4. package/dist/domBackground.js.map +1 -0
  5. package/dist/domBitmap.d.ts +4 -0
  6. package/dist/domBitmap.d.ts.map +1 -0
  7. package/dist/domBitmap.js +77 -0
  8. package/dist/domBitmap.js.map +1 -0
  9. package/dist/domCSSFilterBinding.d.ts +28 -0
  10. package/dist/domCSSFilterBinding.d.ts.map +1 -0
  11. package/dist/domCSSFilterBinding.js +56 -0
  12. package/dist/domCSSFilterBinding.js.map +1 -0
  13. package/dist/domCache.d.ts +14 -0
  14. package/dist/domCache.d.ts.map +1 -0
  15. package/dist/domCache.js +64 -0
  16. package/dist/domCache.js.map +1 -0
  17. package/dist/domClip.d.ts +3 -0
  18. package/dist/domClip.d.ts.map +1 -0
  19. package/dist/domClip.js +38 -0
  20. package/dist/domClip.js.map +1 -0
  21. package/dist/domClipContours.d.ts +6 -0
  22. package/dist/domClipContours.d.ts.map +1 -0
  23. package/dist/domClipContours.js +48 -0
  24. package/dist/domClipContours.js.map +1 -0
  25. package/dist/domClipRectangle.d.ts +6 -0
  26. package/dist/domClipRectangle.d.ts.map +1 -0
  27. package/dist/domClipRectangle.js +148 -0
  28. package/dist/domClipRectangle.js.map +1 -0
  29. package/dist/domDisplayObject.d.ts +5 -0
  30. package/dist/domDisplayObject.d.ts.map +1 -0
  31. package/dist/domDisplayObject.js +60 -0
  32. package/dist/domDisplayObject.js.map +1 -0
  33. package/dist/domFontSource.d.ts +5 -0
  34. package/dist/domFontSource.d.ts.map +1 -0
  35. package/dist/domFontSource.js +18 -0
  36. package/dist/domFontSource.js.map +1 -0
  37. package/dist/domMaterials.d.ts +15 -0
  38. package/dist/domMaterials.d.ts.map +1 -0
  39. package/dist/domMaterials.js +62 -0
  40. package/dist/domMaterials.js.map +1 -0
  41. package/dist/domNativeText.d.ts +5 -0
  42. package/dist/domNativeText.d.ts.map +1 -0
  43. package/dist/domNativeText.js +65 -0
  44. package/dist/domNativeText.js.map +1 -0
  45. package/dist/domReconcile.d.ts +9 -0
  46. package/dist/domReconcile.d.ts.map +1 -0
  47. package/dist/domReconcile.js +69 -0
  48. package/dist/domReconcile.js.map +1 -0
  49. package/dist/domRenderState.d.ts +5 -0
  50. package/dist/domRenderState.d.ts.map +1 -0
  51. package/dist/domRenderState.js +43 -0
  52. package/dist/domRenderState.js.map +1 -0
  53. package/dist/domRenderView.d.ts +4 -0
  54. package/dist/domRenderView.d.ts.map +1 -0
  55. package/dist/domRenderView.js +24 -0
  56. package/dist/domRenderView.js.map +1 -0
  57. package/dist/domRichText.d.ts +7 -0
  58. package/dist/domRichText.d.ts.map +1 -0
  59. package/dist/domRichText.js +183 -0
  60. package/dist/domRichText.js.map +1 -0
  61. package/dist/domScale9Mapper.d.ts +3 -0
  62. package/dist/domScale9Mapper.d.ts.map +1 -0
  63. package/dist/domScale9Mapper.js +37 -0
  64. package/dist/domScale9Mapper.js.map +1 -0
  65. package/dist/domScale9Shape.d.ts +10 -0
  66. package/dist/domScale9Shape.d.ts.map +1 -0
  67. package/dist/domScale9Shape.js +59 -0
  68. package/dist/domScale9Shape.js.map +1 -0
  69. package/dist/domShape.d.ts +4 -0
  70. package/dist/domShape.d.ts.map +1 -0
  71. package/dist/domShape.js +45 -0
  72. package/dist/domShape.js.map +1 -0
  73. package/dist/domStyle.d.ts +9 -0
  74. package/dist/domStyle.d.ts.map +1 -0
  75. package/dist/domStyle.js +25 -0
  76. package/dist/domStyle.js.map +1 -0
  77. package/dist/domSvgFilter.d.ts +27 -0
  78. package/dist/domSvgFilter.d.ts.map +1 -0
  79. package/dist/domSvgFilter.js +88 -0
  80. package/dist/domSvgFilter.js.map +1 -0
  81. package/dist/domTextHelpers.d.ts +6 -0
  82. package/dist/domTextHelpers.d.ts.map +1 -0
  83. package/dist/domTextHelpers.js +8 -0
  84. package/dist/domTextHelpers.js.map +1 -0
  85. package/dist/domTextInput.d.ts +4 -0
  86. package/dist/domTextInput.d.ts.map +1 -0
  87. package/dist/domTextInput.js +60 -0
  88. package/dist/domTextInput.js.map +1 -0
  89. package/dist/domTextLabel.d.ts +4 -0
  90. package/dist/domTextLabel.d.ts.map +1 -0
  91. package/dist/domTextLabel.js +74 -0
  92. package/dist/domTextLabel.js.map +1 -0
  93. package/dist/domTransform.d.ts +4 -0
  94. package/dist/domTransform.d.ts.map +1 -0
  95. package/dist/domTransform.js +16 -0
  96. package/dist/domTransform.js.map +1 -0
  97. package/dist/domVideo.d.ts +4 -0
  98. package/dist/domVideo.d.ts.map +1 -0
  99. package/dist/domVideo.js +27 -0
  100. package/dist/domVideo.js.map +1 -0
  101. package/dist/htmlView.d.ts +4 -0
  102. package/dist/htmlView.d.ts.map +1 -0
  103. package/dist/htmlView.js +28 -0
  104. package/dist/htmlView.js.map +1 -0
  105. package/dist/index.d.ts +24 -0
  106. package/dist/index.d.ts.map +1 -0
  107. package/dist/index.js +24 -0
  108. package/dist/index.js.map +1 -0
  109. package/package.json +49 -0
  110. package/src/domBackground.test.ts +35 -0
  111. package/src/domBitmap.test.ts +117 -0
  112. package/src/domCSSFilterBinding.test.ts +104 -0
  113. package/src/domCache.test.ts +93 -0
  114. package/src/domClip.test.ts +12 -0
  115. package/src/domClipContours.test.ts +62 -0
  116. package/src/domClipRectangle.test.ts +81 -0
  117. package/src/domDisplayObject.test.ts +236 -0
  118. package/src/domFontSource.test.ts +30 -0
  119. package/src/domMaterials.test.ts +111 -0
  120. package/src/domNativeText.test.ts +71 -0
  121. package/src/domReconcile.test.ts +158 -0
  122. package/src/domRenderState.test.ts +87 -0
  123. package/src/domRenderView.test.ts +69 -0
  124. package/src/domRichText.test.ts +165 -0
  125. package/src/domScale9Mapper.test.ts +20 -0
  126. package/src/domScale9Shape.test.ts +76 -0
  127. package/src/domShape.test.ts +85 -0
  128. package/src/domStyle.test.ts +130 -0
  129. package/src/domSvgFilter.test.ts +116 -0
  130. package/src/domTextHelpers.test.ts +27 -0
  131. package/src/domTextInput.test.ts +61 -0
  132. package/src/domTextLabel.test.ts +92 -0
  133. package/src/domTransform.test.ts +68 -0
  134. package/src/domVideo.test.ts +30 -0
  135. package/src/htmlView.test.ts +117 -0
@@ -0,0 +1,85 @@
1
+ import { defaultCanvasShapeCommands, registerCanvasShapeCommands } from '@flighthq/displayobject-canvas';
2
+ import { registerRenderer } from '@flighthq/render';
3
+ import { getOrCreateRenderProxy2D } from '@flighthq/render';
4
+ import { appendShapeBeginFill, appendShapeEndFill, appendShapeRectangle, createShape } from '@flighthq/shape';
5
+ import { ShapeKind } from '@flighthq/types';
6
+
7
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
8
+ import { defaultDomShapeRenderer, drawDomShape } from './domShape';
9
+
10
+ beforeAll(() => {
11
+ registerCanvasShapeCommands(defaultCanvasShapeCommands);
12
+ });
13
+
14
+ function makeState() {
15
+ const container = document.createElement('div');
16
+ const state = createDomRenderState(container);
17
+ registerRenderer(state, ShapeKind, defaultDomShapeRenderer);
18
+ return state;
19
+ }
20
+
21
+ function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
22
+ getDomRenderStateRuntime(state).domCurrentElement = null;
23
+ drawFn();
24
+ return getDomRenderStateRuntime(state).domCurrentElement;
25
+ }
26
+
27
+ describe('defaultDomShapeRenderer', () => {
28
+ it('has submit, and createData', () => {
29
+ expect(typeof defaultDomShapeRenderer.submit).toBe('function');
30
+ expect(typeof defaultDomShapeRenderer.createData).toBe('function');
31
+ });
32
+ });
33
+
34
+ describe('drawDomShape', () => {
35
+ it('does not produce an element when commands array is empty', () => {
36
+ const state = makeState();
37
+ const shape = createShape();
38
+ const renderProxy = getOrCreateRenderProxy2D(state, shape);
39
+
40
+ const el = drawGetEl(state, () => drawDomShape(state, renderProxy));
41
+
42
+ expect(el).toBeNull();
43
+ });
44
+
45
+ it('produces a canvas element when the shape has draw commands', () => {
46
+ const state = makeState();
47
+ const shape = createShape();
48
+ appendShapeBeginFill(shape, 0xff0000);
49
+ appendShapeRectangle(shape, 0, 0, 50, 50);
50
+ appendShapeEndFill(shape);
51
+ const renderProxy = getOrCreateRenderProxy2D(state, shape);
52
+
53
+ const el = drawGetEl(state, () => drawDomShape(state, renderProxy));
54
+
55
+ expect(el).not.toBeNull();
56
+ expect(el!.tagName).toBe('CANVAS');
57
+ });
58
+
59
+ it('sets canvas size to at least 1x1 for zero-size shapes', () => {
60
+ const state = makeState();
61
+ const shape = createShape();
62
+ appendShapeBeginFill(shape, 0xff0000);
63
+ appendShapeRectangle(shape, 0, 0, 0, 0);
64
+ appendShapeEndFill(shape);
65
+ const renderProxy = getOrCreateRenderProxy2D(state, shape);
66
+
67
+ const canvas = drawGetEl(state, () => drawDomShape(state, renderProxy)) as HTMLCanvasElement;
68
+ expect(canvas.width).toBeGreaterThanOrEqual(1);
69
+ expect(canvas.height).toBeGreaterThanOrEqual(1);
70
+ });
71
+
72
+ it('reuses the same canvas element across multiple draws', () => {
73
+ const state = makeState();
74
+ const shape = createShape();
75
+ appendShapeBeginFill(shape, 0xff0000);
76
+ appendShapeRectangle(shape, 0, 0, 40, 40);
77
+ appendShapeEndFill(shape);
78
+ const renderProxy = getOrCreateRenderProxy2D(state, shape);
79
+
80
+ const firstCanvas = drawGetEl(state, () => drawDomShape(state, renderProxy));
81
+ const secondCanvas = drawGetEl(state, () => drawDomShape(state, renderProxy));
82
+
83
+ expect(firstCanvas).toBe(secondCanvas);
84
+ });
85
+ });
@@ -0,0 +1,130 @@
1
+ import { createDisplayObject } from '@flighthq/displayobject';
2
+ import { setMatrix } from '@flighthq/geometry';
3
+ import { getOrCreateRenderProxy2D } from '@flighthq/render';
4
+ import { BlendMode } from '@flighthq/types';
5
+
6
+ import { enableDomCssFilterSupport, setDomCssFilter } from './domCSSFilterBinding';
7
+ import { enableDomBlendModeSupport } from './domMaterials';
8
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
9
+ import { applyDomStyle, prepareDomElement, setDomRendererElement } from './domStyle';
10
+
11
+ function makeState() {
12
+ const container = document.createElement('div');
13
+ return createDomRenderState(container);
14
+ }
15
+
16
+ describe('applyDomStyle', () => {
17
+ it('applies the world transform as a CSS matrix', () => {
18
+ const state = makeState();
19
+ const el = document.createElement('div');
20
+ const obj = createDisplayObject();
21
+ const node = getOrCreateRenderProxy2D(state, obj);
22
+ setMatrix(node.transform2D, 1, 0, 0, 1, 10, 20);
23
+
24
+ applyDomStyle(state, el, node);
25
+
26
+ expect(el.style.transform).toContain('matrix(1,0,0,1,10,20)');
27
+ });
28
+
29
+ it('sets opacity when alpha is less than 1', () => {
30
+ const state = makeState();
31
+ const el = document.createElement('div');
32
+ const obj = createDisplayObject();
33
+ const node = getOrCreateRenderProxy2D(state, obj);
34
+ node.alpha = 0.5;
35
+
36
+ applyDomStyle(state, el, node);
37
+
38
+ expect(el.style.opacity).toBe('0.5');
39
+ });
40
+
41
+ it('clears opacity when alpha is 1', () => {
42
+ const state = makeState();
43
+ const el = document.createElement('div');
44
+ el.style.opacity = '0.5';
45
+ const obj = createDisplayObject();
46
+ const node = getOrCreateRenderProxy2D(state, obj);
47
+ node.alpha = 1;
48
+
49
+ applyDomStyle(state, el, node);
50
+
51
+ expect(el.style.opacity).toBe('');
52
+ });
53
+
54
+ it('sets mixBlendMode for non-default blend mode when blend support is enabled', () => {
55
+ const state = makeState();
56
+ enableDomBlendModeSupport(state);
57
+ const el = document.createElement('div');
58
+ const obj = createDisplayObject();
59
+ const node = getOrCreateRenderProxy2D(state, obj);
60
+ node.blendMode = BlendMode.Multiply;
61
+
62
+ applyDomStyle(state, el, node);
63
+
64
+ expect(el.style.mixBlendMode).toBe('multiply');
65
+ });
66
+
67
+ it('leaves mixBlendMode untouched when blend support is not enabled', () => {
68
+ const state = makeState();
69
+ const el = document.createElement('div');
70
+ const obj = createDisplayObject();
71
+ const node = getOrCreateRenderProxy2D(state, obj);
72
+ node.blendMode = BlendMode.Multiply;
73
+
74
+ applyDomStyle(state, el, node);
75
+
76
+ expect(el.style.mixBlendMode).toBe('');
77
+ });
78
+
79
+ it('applies a bound CSS filter to the element', () => {
80
+ const state = makeState();
81
+ const el = document.createElement('div');
82
+ const obj = createDisplayObject();
83
+ enableDomCssFilterSupport(state);
84
+ setDomCssFilter(state, obj, 'blur(3px)');
85
+ const node = getOrCreateRenderProxy2D(state, obj);
86
+
87
+ applyDomStyle(state, el, node);
88
+
89
+ expect(el.style.filter).toBe('blur(3px)');
90
+ });
91
+ });
92
+
93
+ describe('prepareDomElement', () => {
94
+ it('sets position to absolute', () => {
95
+ const el = document.createElement('div');
96
+ prepareDomElement(el);
97
+ expect(el.style.position).toBe('absolute');
98
+ });
99
+
100
+ it('sets left and top to 0', () => {
101
+ const el = document.createElement('div');
102
+ prepareDomElement(el);
103
+ expect(el.style.left).toBe('0px');
104
+ expect(el.style.top).toBe('0px');
105
+ });
106
+
107
+ it('sets transformOrigin to "0 0"', () => {
108
+ const el = document.createElement('div');
109
+ prepareDomElement(el);
110
+ expect(el.style.transformOrigin).toBe('0 0');
111
+ });
112
+
113
+ it('sets pointerEvents to none', () => {
114
+ const el = document.createElement('div');
115
+ prepareDomElement(el);
116
+ expect(el.style.pointerEvents).toBe('none');
117
+ });
118
+ });
119
+
120
+ describe('setDomRendererElement', () => {
121
+ it('writes the element to domCurrentElement on the internal state', () => {
122
+ const container = document.createElement('div');
123
+ const state = createDomRenderState(container);
124
+ const el = document.createElement('canvas');
125
+
126
+ setDomRendererElement(state, el);
127
+
128
+ expect(getDomRenderStateRuntime(state).domCurrentElement).toBe(el);
129
+ });
130
+ });
@@ -0,0 +1,116 @@
1
+ import { getDomSvgColorMatrixFilter, releaseDomSvgColorMatrixFilter } from './domSvgFilter';
2
+
3
+ // A simple identity color matrix (Flash 0–255 offset convention).
4
+ const IDENTITY_MATRIX = [
5
+ 1,
6
+ 0,
7
+ 0,
8
+ 0,
9
+ 0, // R row
10
+ 0,
11
+ 1,
12
+ 0,
13
+ 0,
14
+ 0, // G row
15
+ 0,
16
+ 0,
17
+ 1,
18
+ 0,
19
+ 0, // B row
20
+ 0,
21
+ 0,
22
+ 0,
23
+ 1,
24
+ 0, // A row
25
+ ];
26
+
27
+ describe('getDomSvgColorMatrixFilter', () => {
28
+ it('returns a css url(#id) reference string', () => {
29
+ const filter = { kind: 'ColorMatrixFilter' as const, matrix: IDENTITY_MATRIX };
30
+ const css = getDomSvgColorMatrixFilter(filter);
31
+ expect(css).toMatch(/^url\(#flighthq-cm-\d+\)$/);
32
+ });
33
+
34
+ it('injects a <filter> element into the document', () => {
35
+ const filter = { kind: 'ColorMatrixFilter' as const, matrix: IDENTITY_MATRIX };
36
+ const css = getDomSvgColorMatrixFilter(filter)!;
37
+ const id = css.slice('url(#'.length, -1);
38
+ const el = document.getElementById(id);
39
+ expect(el).not.toBeNull();
40
+ expect(el!.tagName.toLowerCase()).toBe('filter');
41
+ });
42
+
43
+ it('injects a feColorMatrix child with type=matrix', () => {
44
+ const filter = { kind: 'ColorMatrixFilter' as const, matrix: IDENTITY_MATRIX };
45
+ const css = getDomSvgColorMatrixFilter(filter)!;
46
+ const id = css.slice('url(#'.length, -1);
47
+ const filterEl = document.getElementById(id)!;
48
+ const feEl = filterEl.querySelector('feColorMatrix');
49
+ expect(feEl).not.toBeNull();
50
+ expect(feEl!.getAttribute('type')).toBe('matrix');
51
+ });
52
+
53
+ it('divides offset column by 255 in the feColorMatrix values', () => {
54
+ // Matrix with offset 255 in the red channel (index 4), which should become 1.0 in SVG.
55
+ const matrix = [
56
+ 1,
57
+ 0,
58
+ 0,
59
+ 0,
60
+ 255, // R: offset 255 → should map to 1 in SVG
61
+ 0,
62
+ 1,
63
+ 0,
64
+ 0,
65
+ 0,
66
+ 0,
67
+ 0,
68
+ 1,
69
+ 0,
70
+ 0,
71
+ 0,
72
+ 0,
73
+ 0,
74
+ 1,
75
+ 0,
76
+ ];
77
+ const filter = { kind: 'ColorMatrixFilter' as const, matrix };
78
+ const css = getDomSvgColorMatrixFilter(filter)!;
79
+ const id = css.slice('url(#'.length, -1);
80
+ const filterEl = document.getElementById(id)!;
81
+ const feEl = filterEl.querySelector('feColorMatrix')!;
82
+ const values = feEl.getAttribute('values')!.split(' ').map(Number);
83
+ // Index 4 is the R-row offset; should be 255/255 = 1
84
+ expect(values[4]).toBeCloseTo(1, 5);
85
+ });
86
+
87
+ it('each call produces a unique id', () => {
88
+ const filter = { kind: 'ColorMatrixFilter' as const, matrix: IDENTITY_MATRIX };
89
+ const css1 = getDomSvgColorMatrixFilter(filter);
90
+ const css2 = getDomSvgColorMatrixFilter(filter);
91
+ expect(css1).not.toBe(css2);
92
+ });
93
+
94
+ it('sets color-interpolation-filters to sRGB on the filter element', () => {
95
+ const filter = { kind: 'ColorMatrixFilter' as const, matrix: IDENTITY_MATRIX };
96
+ const css = getDomSvgColorMatrixFilter(filter)!;
97
+ const id = css.slice('url(#'.length, -1);
98
+ const filterEl = document.getElementById(id)!;
99
+ expect(filterEl.getAttribute('color-interpolation-filters')).toBe('sRGB');
100
+ });
101
+ });
102
+
103
+ describe('releaseDomSvgColorMatrixFilter', () => {
104
+ it('removes the injected filter element from the document', () => {
105
+ const filter = { kind: 'ColorMatrixFilter' as const, matrix: IDENTITY_MATRIX };
106
+ const css = getDomSvgColorMatrixFilter(filter)!;
107
+ const id = css.slice('url(#'.length, -1);
108
+ expect(document.getElementById(id)).not.toBeNull();
109
+ releaseDomSvgColorMatrixFilter(id);
110
+ expect(document.getElementById(id)).toBeNull();
111
+ });
112
+
113
+ it('is a no-op for an unknown id', () => {
114
+ expect(() => releaseDomSvgColorMatrixFilter('nonexistent-filter-id')).not.toThrow();
115
+ });
116
+ });
@@ -0,0 +1,27 @@
1
+ import { escapeDomHtmlString } from './domTextHelpers';
2
+
3
+ describe('escapeDomHtmlString', () => {
4
+ it('escapes ampersands', () => {
5
+ expect(escapeDomHtmlString('a&b')).toBe('a&amp;b');
6
+ });
7
+
8
+ it('escapes greater-than signs', () => {
9
+ expect(escapeDomHtmlString('a>b')).toBe('a&gt;b');
10
+ });
11
+
12
+ it('escapes less-than signs', () => {
13
+ expect(escapeDomHtmlString('a<b')).toBe('a&lt;b');
14
+ });
15
+
16
+ it('escapes multiple special characters in sequence', () => {
17
+ expect(escapeDomHtmlString('<a & b>')).toBe('&lt;a&nbsp;&amp;&nbsp;b&gt;');
18
+ });
19
+
20
+ it('escapes spaces to &nbsp;', () => {
21
+ expect(escapeDomHtmlString('a b')).toBe('a&nbsp;b');
22
+ });
23
+
24
+ it('returns unchanged string when no special characters', () => {
25
+ expect(escapeDomHtmlString('hello')).toBe('hello');
26
+ });
27
+ });
@@ -0,0 +1,61 @@
1
+ import { getOrCreateRenderProxy2D, registerRenderer } from '@flighthq/render';
2
+ import { createRichText } from '@flighthq/text';
3
+ import { enableTextInput, setTextInputSelection } from '@flighthq/textinput';
4
+ import type { RichText } from '@flighthq/types';
5
+ import { RichTextKind } from '@flighthq/types';
6
+
7
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
8
+ import { defaultDomRichTextRenderer, drawDomRichText } from './domRichText';
9
+ import { drawDomTextInputOverlay, enableDomTextInput } from './domTextInput';
10
+
11
+ function makeState() {
12
+ const state = createDomRenderState(document.createElement('div'));
13
+ registerRenderer(state, RichTextKind, defaultDomRichTextRenderer);
14
+ return state;
15
+ }
16
+
17
+ function makeFocusedInput(text: string): RichText {
18
+ const node = createRichText({ data: { text, width: 100, height: 40 } });
19
+ enableTextInput(node).focused = true;
20
+ return node;
21
+ }
22
+
23
+ function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
24
+ getDomRenderStateRuntime(state).domCurrentElement = null;
25
+ drawFn();
26
+ return getDomRenderStateRuntime(state).domCurrentElement;
27
+ }
28
+
29
+ describe('drawDomTextInputOverlay', () => {
30
+ it('is the installed overlay function', () => {
31
+ expect(typeof drawDomTextInputOverlay).toBe('function');
32
+ });
33
+
34
+ it('appends a caret for the focused collapsed selection', () => {
35
+ enableDomTextInput();
36
+ const state = makeState();
37
+ const node = makeFocusedInput('hello');
38
+ setTextInputSelection(node, 2, 2);
39
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
40
+
41
+ const div = drawGetEl(state, () => drawDomRichText(state, renderProxy));
42
+ expect(div!.innerHTML).toContain('background:#000000');
43
+ });
44
+
45
+ it('appends selection rectangles for the focused expanded selection', () => {
46
+ enableDomTextInput();
47
+ const state = makeState();
48
+ const node = makeFocusedInput('hello');
49
+ setTextInputSelection(node, 1, 4);
50
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
51
+
52
+ const div = drawGetEl(state, () => drawDomRichText(state, renderProxy));
53
+ expect(div!.innerHTML).toContain('background:#0078d7');
54
+ });
55
+ });
56
+
57
+ describe('enableDomTextInput', () => {
58
+ it('installs the overlay without throwing', () => {
59
+ expect(() => enableDomTextInput()).not.toThrow();
60
+ });
61
+ });
@@ -0,0 +1,92 @@
1
+ import { registerRenderer } from '@flighthq/render';
2
+ import { getOrCreateRenderProxy2D } from '@flighthq/render';
3
+ import { createTextLabel } from '@flighthq/text';
4
+ import { TextLabelKind } from '@flighthq/types';
5
+
6
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
7
+ import { defaultDomTextLabelRenderer, drawDomTextLabel } from './domTextLabel';
8
+
9
+ function makeState() {
10
+ const container = document.createElement('div');
11
+ const state = createDomRenderState(container);
12
+ registerRenderer(state, TextLabelKind, defaultDomTextLabelRenderer);
13
+ return state;
14
+ }
15
+
16
+ function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
17
+ getDomRenderStateRuntime(state).domCurrentElement = null;
18
+ drawFn();
19
+ return getDomRenderStateRuntime(state).domCurrentElement;
20
+ }
21
+
22
+ describe('defaultDomTextLabelRenderer', () => {
23
+ it('has submit, and createData', () => {
24
+ expect(typeof defaultDomTextLabelRenderer.submit).toBe('function');
25
+ expect(typeof defaultDomTextLabelRenderer.createData).toBe('function');
26
+ });
27
+ });
28
+
29
+ describe('drawDomTextLabel', () => {
30
+ it('does not throw when text is empty', () => {
31
+ const state = makeState();
32
+ const node = createTextLabel();
33
+ node.data.text = '';
34
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
35
+ expect(() => drawDomTextLabel(state, renderProxy)).not.toThrow();
36
+ });
37
+
38
+ it('produces no element when text is empty', () => {
39
+ const state = makeState();
40
+ const node = createTextLabel();
41
+ node.data.text = '';
42
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
43
+
44
+ const el = drawGetEl(state, () => drawDomTextLabel(state, renderProxy));
45
+
46
+ expect(el).toBeNull();
47
+ });
48
+
49
+ it('produces a div when text is non-empty', () => {
50
+ const state = makeState();
51
+ const node = createTextLabel();
52
+ node.data.text = 'hello';
53
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
54
+
55
+ const el = drawGetEl(state, () => drawDomTextLabel(state, renderProxy));
56
+
57
+ expect(el).not.toBeNull();
58
+ expect(el!.tagName).toBe('DIV');
59
+ });
60
+
61
+ it('sets overflow:hidden on the div', () => {
62
+ const state = makeState();
63
+ const node = createTextLabel();
64
+ node.data.text = 'hello';
65
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
66
+
67
+ const div = drawGetEl(state, () => drawDomTextLabel(state, renderProxy))!;
68
+ expect(div.style.overflow).toBe('hidden');
69
+ });
70
+
71
+ it('includes the text content in innerHtml', () => {
72
+ const state = makeState();
73
+ const node = createTextLabel();
74
+ node.data.text = 'world';
75
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
76
+
77
+ const div = drawGetEl(state, () => drawDomTextLabel(state, renderProxy))!;
78
+ expect(div.innerHTML).toContain('world');
79
+ });
80
+
81
+ it('reuses the same div across multiple draws', () => {
82
+ const state = makeState();
83
+ const node = createTextLabel();
84
+ node.data.text = 'hello';
85
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
86
+
87
+ const firstDiv = drawGetEl(state, () => drawDomTextLabel(state, renderProxy));
88
+ const secondDiv = drawGetEl(state, () => drawDomTextLabel(state, renderProxy));
89
+
90
+ expect(firstDiv).toBe(secondDiv);
91
+ });
92
+ });
@@ -0,0 +1,68 @@
1
+ import { createMatrix } from '@flighthq/geometry';
2
+
3
+ import { setDomTransform, setDomTransformWithOffset } from './domTransform';
4
+
5
+ describe('setDomTransform', () => {
6
+ it('sets the CSS transform to the matrix values', () => {
7
+ const el = document.createElement('div');
8
+ const t = createMatrix(1, 2, 3, 4, 5, 6);
9
+
10
+ setDomTransform(el, t, false);
11
+
12
+ expect(el.style.transform).toBe('matrix(1,2,3,4,5,6)');
13
+ });
14
+
15
+ it('passes tx and ty through without rounding when roundPixels is false', () => {
16
+ const el = document.createElement('div');
17
+ const t = createMatrix(1, 0, 0, 1, 1.7, 2.3);
18
+
19
+ setDomTransform(el, t, false);
20
+
21
+ expect(el.style.transform).toBe('matrix(1,0,0,1,1.7,2.3)');
22
+ });
23
+
24
+ it('frounds tx and ty when roundPixels is true', () => {
25
+ const el = document.createElement('div');
26
+ const t = createMatrix(1, 0, 0, 1, 5.6789, 9.1011);
27
+
28
+ setDomTransform(el, t, true);
29
+
30
+ const expectedTx = Math.fround(5.6789);
31
+ const expectedTy = Math.fround(9.1011);
32
+ expect(el.style.transform).toBe(`matrix(1,0,0,1,${expectedTx},${expectedTy})`);
33
+ });
34
+ });
35
+
36
+ describe('setDomTransformWithOffset', () => {
37
+ it('applies offset through the transform before setting CSS matrix', () => {
38
+ const el = document.createElement('div');
39
+ const t = createMatrix(1, 0, 0, 1, 10, 20);
40
+
41
+ setDomTransformWithOffset(el, t, 5, 8, false);
42
+
43
+ // tx = 1*5 + 0*8 + 10 = 15, ty = 0*5 + 1*8 + 20 = 28
44
+ expect(el.style.transform).toBe('matrix(1,0,0,1,15,28)');
45
+ });
46
+
47
+ it('applies offset through a rotated transform', () => {
48
+ const el = document.createElement('div');
49
+ // 90-degree rotation: a=0, b=1, c=-1, d=0
50
+ const t = createMatrix(0, 1, -1, 0, 0, 0);
51
+
52
+ setDomTransformWithOffset(el, t, 10, 20, false);
53
+
54
+ // tx = 0*10 + (-1)*20 + 0 = -20, ty = 1*10 + 0*20 + 0 = 10
55
+ expect(el.style.transform).toBe('matrix(0,1,-1,0,-20,10)');
56
+ });
57
+
58
+ it('frounds the adjusted tx/ty when roundPixels is true', () => {
59
+ const el = document.createElement('div');
60
+ const t = createMatrix(1, 0, 0, 1, 0.5, 0.5);
61
+
62
+ setDomTransformWithOffset(el, t, 0.1, 0.1, true);
63
+
64
+ const expectedTx = Math.fround(0.6);
65
+ const expectedTy = Math.fround(0.6);
66
+ expect(el.style.transform).toBe(`matrix(1,0,0,1,${expectedTx},${expectedTy})`);
67
+ });
68
+ });
@@ -0,0 +1,30 @@
1
+ import { createVideo } from '@flighthq/displayobject';
2
+ import { registerRenderer } from '@flighthq/render';
3
+ import { getOrCreateRenderProxy2D } from '@flighthq/render';
4
+ import { VideoKind } from '@flighthq/types';
5
+
6
+ import { createDomRenderState } from './domRenderState';
7
+ import { defaultDomVideoRenderer, drawDomVideo } from './domVideo';
8
+
9
+ function makeState() {
10
+ const container = document.createElement('div');
11
+ const state = createDomRenderState(container);
12
+ registerRenderer(state, VideoKind, defaultDomVideoRenderer);
13
+ return state;
14
+ }
15
+
16
+ describe('defaultDomVideoRenderer', () => {
17
+ it('has submit, and createData functions', () => {
18
+ expect(typeof defaultDomVideoRenderer.submit).toBe('function');
19
+ expect(typeof defaultDomVideoRenderer.createData).toBe('function');
20
+ });
21
+ });
22
+
23
+ describe('drawDomVideo', () => {
24
+ it('does not throw when source element is null', () => {
25
+ const state = makeState();
26
+ const video = createVideo();
27
+ const renderProxy = getOrCreateRenderProxy2D(state, video);
28
+ expect(() => drawDomVideo(state, renderProxy)).not.toThrow();
29
+ });
30
+ });