@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,71 @@
1
+ import { getOrCreateRenderProxy2D, registerRenderer } from '@flighthq/render';
2
+ import { createNativeText } from '@flighthq/text';
3
+ import { NativeTextKind } from '@flighthq/types';
4
+
5
+ import { defaultDomNativeTextRenderer, drawDomNativeText, drawDomNativeTextMask } from './domNativeText';
6
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
7
+
8
+ function makeState() {
9
+ const container = document.createElement('div');
10
+ const state = createDomRenderState(container);
11
+ registerRenderer(state, NativeTextKind, defaultDomNativeTextRenderer);
12
+ return state;
13
+ }
14
+
15
+ function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
16
+ getDomRenderStateRuntime(state).domCurrentElement = null;
17
+ drawFn();
18
+ return getDomRenderStateRuntime(state).domCurrentElement;
19
+ }
20
+
21
+ describe('defaultDomNativeTextRenderer', () => {
22
+ it('has submit and createData', () => {
23
+ expect(typeof defaultDomNativeTextRenderer.submit).toBe('function');
24
+ expect(typeof defaultDomNativeTextRenderer.createData).toBe('function');
25
+ });
26
+ });
27
+
28
+ describe('drawDomNativeText', () => {
29
+ it('produces a div carrying the text content', () => {
30
+ const state = makeState();
31
+ const node = createNativeText({ data: { text: 'hello' } });
32
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
33
+
34
+ const el = drawGetEl(state, () => drawDomNativeText(state, renderProxy));
35
+
36
+ expect(el).not.toBeNull();
37
+ expect(el!.tagName).toBe('DIV');
38
+ expect(el!.textContent).toBe('hello');
39
+ });
40
+
41
+ it('reuses the same element across multiple draws', () => {
42
+ const state = makeState();
43
+ const node = createNativeText({ data: { text: 'hello' } });
44
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
45
+
46
+ const first = drawGetEl(state, () => drawDomNativeText(state, renderProxy));
47
+ const second = drawGetEl(state, () => drawDomNativeText(state, renderProxy));
48
+
49
+ expect(first).toBe(second);
50
+ });
51
+
52
+ it('sets a fixed box under autoSize none', () => {
53
+ const state = makeState();
54
+ const node = createNativeText({ data: { text: 'hi', width: 120, height: 40 } });
55
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
56
+
57
+ const div = drawGetEl(state, () => drawDomNativeText(state, renderProxy))!;
58
+ expect(div.style.width).toBe('120px');
59
+ expect(div.style.height).toBe('40px');
60
+ expect(div.style.overflow).toBe('hidden');
61
+ });
62
+ });
63
+
64
+ describe('drawDomNativeTextMask', () => {
65
+ it('does not throw', () => {
66
+ const state = makeState();
67
+ const node = createNativeText({ data: { text: 'hi' } });
68
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
69
+ expect(() => drawDomNativeTextMask(state, renderProxy)).not.toThrow();
70
+ });
71
+ });
@@ -0,0 +1,158 @@
1
+ import type { DomRenderStateRuntime } from '@flighthq/types';
2
+
3
+ import { hasDomStructureChanged, processDomNode, reconcileDomContainer, swapDomOrderLists } from './domReconcile';
4
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
5
+
6
+ function makeRuntime(): DomRenderStateRuntime {
7
+ const container = document.createElement('div');
8
+ return getDomRenderStateRuntime(createDomRenderState(container));
9
+ }
10
+
11
+ describe('hasDomStructureChanged', () => {
12
+ it('returns true when needsReconcile is already set', () => {
13
+ const runtime = makeRuntime();
14
+ expect(hasDomStructureChanged(runtime, 0, true)).toBe(true);
15
+ });
16
+
17
+ it('returns true when lengths differ', () => {
18
+ const runtime = makeRuntime();
19
+ runtime.domOrderLength = 2;
20
+ expect(hasDomStructureChanged(runtime, 1, false)).toBe(true);
21
+ });
22
+
23
+ it('returns false when order lists are identical', () => {
24
+ const runtime = makeRuntime();
25
+ const node = {} as any;
26
+ runtime.domOrderLength = 1;
27
+ runtime.domOrderList[0] = node;
28
+ runtime.domNextOrderList[0] = node;
29
+ expect(hasDomStructureChanged(runtime, 1, false)).toBe(false);
30
+ });
31
+
32
+ it('returns true when a node differs at any position', () => {
33
+ const runtime = makeRuntime();
34
+ runtime.domOrderLength = 1;
35
+ runtime.domOrderList[0] = {} as any;
36
+ runtime.domNextOrderList[0] = {} as any;
37
+ expect(hasDomStructureChanged(runtime, 1, false)).toBe(true);
38
+ });
39
+ });
40
+
41
+ describe('processDomNode', () => {
42
+ it('calls drawFn and registers the element for a new node', () => {
43
+ const runtime = makeRuntime();
44
+ const node = {} as any;
45
+ const el = document.createElement('div');
46
+
47
+ const result = processDomNode(
48
+ runtime,
49
+ node,
50
+ 0,
51
+ () => {
52
+ runtime.domCurrentElement = el;
53
+ },
54
+ 0,
55
+ );
56
+
57
+ expect(result.newLength).toBe(1);
58
+ expect(result.needsReconcile).toBe(true);
59
+ expect(runtime.domElementMap.get(node)).toBe(el);
60
+ expect(runtime.domNextOrderList[0]).toBe(node);
61
+ });
62
+
63
+ it('skips drawFn for a known node with no dirty frames', () => {
64
+ const runtime = makeRuntime();
65
+ const node = { appearanceFrameId: -1, transformFrameId: -1 } as any;
66
+ const el = document.createElement('div');
67
+ runtime.domElementMap.set(node, el);
68
+
69
+ let called = false;
70
+ const result = processDomNode(
71
+ runtime,
72
+ node,
73
+ 0,
74
+ () => {
75
+ called = true;
76
+ },
77
+ 0,
78
+ );
79
+
80
+ expect(called).toBe(false);
81
+ expect(result.needsReconcile).toBe(false);
82
+ expect(result.newLength).toBe(1);
83
+ });
84
+
85
+ it('sets needsReconcile when the element changes for a known node', () => {
86
+ const runtime = makeRuntime();
87
+ const node = { appearanceFrameId: 1, transformFrameId: -1 } as any;
88
+ const oldEl = document.createElement('div');
89
+ const newEl = document.createElement('canvas');
90
+ runtime.domElementMap.set(node, oldEl);
91
+
92
+ const result = processDomNode(
93
+ runtime,
94
+ node,
95
+ 1,
96
+ () => {
97
+ runtime.domCurrentElement = newEl;
98
+ },
99
+ 0,
100
+ );
101
+
102
+ expect(result.needsReconcile).toBe(true);
103
+ expect(runtime.domElementMap.get(node)).toBe(newEl);
104
+ });
105
+ });
106
+
107
+ describe('reconcileDomContainer', () => {
108
+ it('removes foreign elements not in the new element set', () => {
109
+ const runtime = makeRuntime();
110
+ const container = document.createElement('div');
111
+ const foreign = document.createElement('span');
112
+ container.appendChild(foreign);
113
+
114
+ reconcileDomContainer(container, runtime, 0);
115
+
116
+ expect(container.contains(foreign)).toBe(false);
117
+ });
118
+
119
+ it('places tracked elements in correct order', () => {
120
+ const runtime = makeRuntime();
121
+ const container = document.createElement('div');
122
+ const elA = document.createElement('div');
123
+ const elB = document.createElement('span');
124
+ const nodeA = {} as any;
125
+ const nodeB = {} as any;
126
+ runtime.domElementMap.set(nodeA, elA);
127
+ runtime.domElementMap.set(nodeB, elB);
128
+ runtime.domNextOrderList[0] = nodeA;
129
+ runtime.domNextOrderList[1] = nodeB;
130
+
131
+ reconcileDomContainer(container, runtime, 2);
132
+
133
+ const children = Array.from(container.children);
134
+ expect(children.indexOf(elA)).toBeLessThan(children.indexOf(elB));
135
+ });
136
+ });
137
+
138
+ describe('swapDomOrderLists', () => {
139
+ it('promotes the next list to the current list', () => {
140
+ const runtime = makeRuntime();
141
+ const node = {} as any;
142
+ runtime.domNextOrderList[0] = node;
143
+
144
+ swapDomOrderLists(runtime, 1);
145
+
146
+ expect(runtime.domOrderList[0]).toBe(node);
147
+ expect(runtime.domOrderLength).toBe(1);
148
+ });
149
+
150
+ it('recycles the old current list as the next scratch buffer', () => {
151
+ const runtime = makeRuntime();
152
+ const prevList = runtime.domOrderList;
153
+
154
+ swapDomOrderLists(runtime, 0);
155
+
156
+ expect(runtime.domNextOrderList).toBe(prevList);
157
+ });
158
+ });
@@ -0,0 +1,87 @@
1
+ import { EntityRuntimeKey } from '@flighthq/types';
2
+
3
+ import { createDomRenderState, createDomRenderStateRuntime, getDomRenderStateRuntime } from './domRenderState';
4
+
5
+ describe('createDomRenderState', () => {
6
+ it('returns a state with the provided element', () => {
7
+ const div = document.createElement('div');
8
+ const state = createDomRenderState(div);
9
+ expect(state.element).toBe(div);
10
+ });
11
+
12
+ it('sets position:relative and overflow:hidden on the element', () => {
13
+ const div = document.createElement('div');
14
+ createDomRenderState(div);
15
+ expect(div.style.position).toBe('relative');
16
+ expect(div.style.overflow).toBe('hidden');
17
+ });
18
+
19
+ it('defaults roundPixels to false', () => {
20
+ const div = document.createElement('div');
21
+ const state = createDomRenderState(div);
22
+ expect(state.roundPixels).toBe(false);
23
+ });
24
+
25
+ it('sets roundPixels from options', () => {
26
+ const div = document.createElement('div');
27
+ const state = createDomRenderState(div, { roundPixels: true });
28
+ expect(state.roundPixels).toBe(true);
29
+ });
30
+
31
+ it('defaults the runtime currentBlendMode to null', () => {
32
+ const div = document.createElement('div');
33
+ const state = createDomRenderState(div);
34
+ expect(getDomRenderStateRuntime(state).currentBlendMode).toBeNull();
35
+ });
36
+
37
+ it('attaches a populated runtime under EntityRuntimeKey', () => {
38
+ const div = document.createElement('div');
39
+ const state = createDomRenderState(div);
40
+ const runtime = getDomRenderStateRuntime(state);
41
+ expect(runtime.domClipHooks).toBeNull();
42
+ expect(runtime.domCurrentElement).toBeNull();
43
+ expect(runtime.domOrderLength).toBe(-1);
44
+ expect(runtime.domClipStack).toEqual([]);
45
+ expect(runtime.domOrderList).toEqual([]);
46
+ expect(runtime.domNextOrderList).toEqual([]);
47
+ });
48
+
49
+ it('defaults backgroundColor to 0', () => {
50
+ const div = document.createElement('div');
51
+ const state = createDomRenderState(div);
52
+ expect(state.backgroundColor).toBe(0);
53
+ });
54
+
55
+ it('sets backgroundColor from options', () => {
56
+ const div = document.createElement('div');
57
+ const state = createDomRenderState(div, { backgroundColor: 0xff0000ff });
58
+ expect(state.backgroundColor).toBe(0xff0000ff);
59
+ });
60
+
61
+ it('sets pixelRatio from options', () => {
62
+ const div = document.createElement('div');
63
+ const state = createDomRenderState(div, { pixelRatio: 3 });
64
+ expect(state.pixelRatio).toBe(3);
65
+ });
66
+
67
+ it('defaults pixelRatio to 1', () => {
68
+ const div = document.createElement('div');
69
+ const state = createDomRenderState(div);
70
+ expect(state.pixelRatio).toBe(1);
71
+ });
72
+ });
73
+
74
+ describe('createDomRenderStateRuntime', () => {
75
+ it('allocates an entity runtime with a null binding', () => {
76
+ const runtime = createDomRenderStateRuntime();
77
+ expect(runtime.binding).toBeNull();
78
+ });
79
+ });
80
+
81
+ describe('getDomRenderStateRuntime', () => {
82
+ it('returns the runtime attached under EntityRuntimeKey', () => {
83
+ const div = document.createElement('div');
84
+ const state = createDomRenderState(div);
85
+ expect(getDomRenderStateRuntime(state)).toBe(state[EntityRuntimeKey]);
86
+ });
87
+ });
@@ -0,0 +1,69 @@
1
+ import { createRenderView } from '@flighthq/displayobject';
2
+ import { getOrCreateRenderProxy2D, registerRenderer } from '@flighthq/render';
3
+ import type { RenderViewRenderer } from '@flighthq/types';
4
+ import { RenderViewKind } from '@flighthq/types';
5
+
6
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
7
+ import { defaultDomRenderViewRenderer, drawDomRenderView } from './domRenderView';
8
+
9
+ function makeState() {
10
+ const container = document.createElement('div');
11
+ const state = createDomRenderState(container);
12
+ registerRenderer(state, RenderViewKind, defaultDomRenderViewRenderer);
13
+ return state;
14
+ }
15
+
16
+ function makeRenderer(width = 100, height = 100): RenderViewRenderer {
17
+ const canvas = document.createElement('canvas');
18
+ canvas.style.width = `${width}px`;
19
+ canvas.style.height = `${height}px`;
20
+ canvas.width = width;
21
+ canvas.height = height;
22
+ return { canvas, render: () => {} };
23
+ }
24
+
25
+ function getElement(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
26
+ getDomRenderStateRuntime(state).domCurrentElement = null;
27
+ drawFn();
28
+ return getDomRenderStateRuntime(state).domCurrentElement;
29
+ }
30
+
31
+ describe('drawDomRenderView', () => {
32
+ it('returns no element when renderer is null', () => {
33
+ const state = makeState();
34
+ const view = createRenderView();
35
+ const renderProxy = getOrCreateRenderProxy2D(state, view);
36
+ const el = getElement(state, () => drawDomRenderView(state, renderProxy));
37
+ expect(el).toBeNull();
38
+ });
39
+
40
+ it('calls render() on the renderer', () => {
41
+ const state = makeState();
42
+ const renderer = makeRenderer();
43
+ const renderSpy = vi.spyOn(renderer, 'render');
44
+ const view = createRenderView({ data: { renderer, width: 100, height: 100 } });
45
+ const renderProxy = getOrCreateRenderProxy2D(state, view);
46
+ drawDomRenderView(state, renderProxy);
47
+ expect(renderSpy).toHaveBeenCalledOnce();
48
+ });
49
+
50
+ it('returns the renderer canvas as the DOM element', () => {
51
+ const state = makeState();
52
+ const renderer = makeRenderer();
53
+ const view = createRenderView({ data: { renderer, width: 100, height: 100 } });
54
+ const renderProxy = getOrCreateRenderProxy2D(state, view);
55
+ const el = getElement(state, () => drawDomRenderView(state, renderProxy));
56
+ expect(el).toBe(renderer.canvas);
57
+ });
58
+
59
+ it('initializes canvas positioning on first draw', () => {
60
+ const state = makeState();
61
+ const renderer = makeRenderer();
62
+ const view = createRenderView({ data: { renderer, width: 100, height: 100 } });
63
+ const renderProxy = getOrCreateRenderProxy2D(state, view);
64
+ drawDomRenderView(state, renderProxy);
65
+ expect(renderer.canvas.style.position).toBe('absolute');
66
+ expect(renderer.canvas.style.transformOrigin).toBe('0 0');
67
+ expect(renderer.canvas.style.pointerEvents).toBe('none');
68
+ });
69
+ });
@@ -0,0 +1,165 @@
1
+ import { registerRenderer } from '@flighthq/render';
2
+ import { getOrCreateRenderProxy2D } from '@flighthq/render';
3
+ import { createRichText } from '@flighthq/text';
4
+ import { enableTextInput } from '@flighthq/textinput';
5
+ import { RichTextKind } from '@flighthq/types';
6
+
7
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
8
+ import {
9
+ defaultDomRichTextRenderer,
10
+ drawDomRichText,
11
+ drawDomRichTextMask,
12
+ registerDomTextInputOverlay,
13
+ } from './domRichText';
14
+
15
+ function makeState() {
16
+ const container = document.createElement('div');
17
+ const state = createDomRenderState(container);
18
+ registerRenderer(state, RichTextKind, defaultDomRichTextRenderer);
19
+ return state;
20
+ }
21
+
22
+ function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
23
+ getDomRenderStateRuntime(state).domCurrentElement = null;
24
+ drawFn();
25
+ return getDomRenderStateRuntime(state).domCurrentElement;
26
+ }
27
+
28
+ describe('defaultDomRichTextRenderer', () => {
29
+ it('has submit, and createData', () => {
30
+ expect(typeof defaultDomRichTextRenderer.submit).toBe('function');
31
+ expect(typeof defaultDomRichTextRenderer.createData).toBe('function');
32
+ });
33
+ });
34
+
35
+ describe('drawDomRichText', () => {
36
+ it('does not throw when text is empty', () => {
37
+ const state = makeState();
38
+ const node = createRichText();
39
+ node.data.text = '';
40
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
41
+ expect(() => drawDomRichText(state, renderProxy)).not.toThrow();
42
+ });
43
+
44
+ it('produces a div even when text is empty', () => {
45
+ const state = makeState();
46
+ const node = createRichText();
47
+ node.data.text = '';
48
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
49
+
50
+ const el = drawGetEl(state, () => drawDomRichText(state, renderProxy));
51
+
52
+ expect(el).not.toBeNull();
53
+ });
54
+
55
+ it('clears innerHtml when text is empty', () => {
56
+ const state = makeState();
57
+ const node = createRichText();
58
+ node.data.text = 'hello';
59
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
60
+ drawDomRichText(state, renderProxy);
61
+
62
+ node.data.text = '';
63
+ const div = drawGetEl(state, () => drawDomRichText(state, renderProxy));
64
+
65
+ expect(div!.innerHTML).toBe('');
66
+ });
67
+
68
+ it('produces a div when text is non-empty', () => {
69
+ const state = makeState();
70
+ const node = createRichText();
71
+ node.data.text = 'hello';
72
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
73
+
74
+ const el = drawGetEl(state, () => drawDomRichText(state, renderProxy));
75
+
76
+ expect(el).not.toBeNull();
77
+ expect(el!.tagName).toBe('DIV');
78
+ });
79
+
80
+ it('sets div width and height from source data', () => {
81
+ const state = makeState();
82
+ const node = createRichText();
83
+ node.data.text = 'hello';
84
+ node.data.width = 200;
85
+ node.data.height = 100;
86
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
87
+
88
+ const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
89
+ expect(div.style.width).toBe('200px');
90
+ expect(div.style.height).toBe('100px');
91
+ });
92
+
93
+ it('includes the text content in innerHtml', () => {
94
+ const state = makeState();
95
+ const node = createRichText();
96
+ node.data.text = 'world';
97
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
98
+
99
+ const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
100
+ expect(div.innerHTML).toContain('world');
101
+ });
102
+
103
+ it('renders resolved htmlText spans', () => {
104
+ const state = makeState();
105
+ const node = createRichText();
106
+ node.data.htmlText = '<b>Bold</b><font color="#00ff00">Green</font>';
107
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
108
+
109
+ const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
110
+ expect(div.innerHTML).toContain('Bold');
111
+ expect(div.innerHTML).toContain('Green');
112
+ expect(div.innerHTML).toContain('bold');
113
+ expect(div.innerHTML).toContain('#00ff00');
114
+ });
115
+
116
+ it('sets backgroundColor when background is enabled', () => {
117
+ const state = makeState();
118
+ const node = createRichText();
119
+ node.data.text = 'hi';
120
+ node.data.background = true;
121
+ node.data.backgroundColor = 0xff0000;
122
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
123
+
124
+ const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
125
+ expect(div.style.backgroundColor).not.toBe('');
126
+ });
127
+
128
+ it('clears backgroundColor when background is disabled', () => {
129
+ const state = makeState();
130
+ const node = createRichText();
131
+ node.data.text = 'hi';
132
+ node.data.background = false;
133
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
134
+
135
+ const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
136
+ expect(div.style.backgroundColor).toBe('');
137
+ });
138
+ });
139
+
140
+ describe('drawDomRichTextMask', () => {
141
+ it('does not throw and produces a DOM element', () => {
142
+ const state = makeState();
143
+ const node = createRichText({ data: { text: 'mask text' } });
144
+ const renderProxy = getOrCreateRenderProxy2D(state, node);
145
+ const el = drawGetEl(state, () => drawDomRichTextMask(state, renderProxy));
146
+ expect(el).not.toBeNull();
147
+ });
148
+ });
149
+
150
+ describe('registerDomTextInputOverlay', () => {
151
+ it('invokes the registered overlay only for a RichText with an input slot', () => {
152
+ const overlay = vi.fn();
153
+ registerDomTextInputOverlay(overlay);
154
+ const state = makeState();
155
+
156
+ const plain = createRichText({ data: { text: 'x' } });
157
+ drawDomRichText(state, getOrCreateRenderProxy2D(state, plain));
158
+ expect(overlay).not.toHaveBeenCalled();
159
+
160
+ const editable = createRichText({ data: { text: 'x' } });
161
+ enableTextInput(editable);
162
+ drawDomRichText(state, getOrCreateRenderProxy2D(state, editable));
163
+ expect(overlay).toHaveBeenCalled();
164
+ });
165
+ });
@@ -0,0 +1,20 @@
1
+ import { createRectangle } from '@flighthq/geometry';
2
+
3
+ import { buildDomScale9Mapper } from './domScale9Mapper';
4
+
5
+ describe('buildDomScale9Mapper', () => {
6
+ it('returns null for invalid scale or bounds', () => {
7
+ expect(buildDomScale9Mapper(createRectangle(0, 0, 0, 100), createRectangle(10, 10, 20, 20), 1, 1)).toBeNull();
8
+ expect(buildDomScale9Mapper(createRectangle(0, 0, 100, 100), createRectangle(10, 10, 20, 20), 0, 1)).toBeNull();
9
+ });
10
+
11
+ it('maps edge and center coordinates using 9-slice scaling', () => {
12
+ const mapper = buildDomScale9Mapper(createRectangle(0, 0, 100, 100), createRectangle(10, 10, 20, 20), 2, 3)!;
13
+
14
+ expect(mapper.mapX(5)).toBe(5);
15
+ expect(mapper.mapX(20)).toBe(70);
16
+ expect(mapper.mapX(40)).toBe(140);
17
+ expect(mapper.mapY(20)).toBe(120);
18
+ expect(mapper.mapY(40)).toBe(240);
19
+ });
20
+ });
@@ -0,0 +1,76 @@
1
+ import { getOrCreateRenderProxy2D } from '@flighthq/render';
2
+ import { appendShapeBeginFill, appendShapeRectangle, createScale9Shape } from '@flighthq/shape';
3
+
4
+ import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
5
+ import { createDomScale9ShapeData, defaultDomScale9ShapeRenderer, drawDomScale9Shape } from './domScale9Shape';
6
+
7
+ const grid = { height: 80, width: 80, x: 10, y: 10 };
8
+
9
+ function getCurrentElement(state: ReturnType<typeof createDomRenderState>): HTMLElement | null {
10
+ return getDomRenderStateRuntime(state).domCurrentElement;
11
+ }
12
+
13
+ describe('createDomScale9ShapeData', () => {
14
+ it('creates empty renderer data', () => {
15
+ const state = createDomRenderState(document.createElement('div'));
16
+ const shape = createScale9Shape(grid);
17
+
18
+ const data = createDomScale9ShapeData(state, shape);
19
+
20
+ expect(data.canvas).toBeNull();
21
+ expect(data.context).toBeNull();
22
+ });
23
+ });
24
+
25
+ describe('defaultDomScale9ShapeRenderer', () => {
26
+ it('has submit, and createData functions', () => {
27
+ expect(defaultDomScale9ShapeRenderer.createData).toBe(createDomScale9ShapeData);
28
+ expect(defaultDomScale9ShapeRenderer.submit).toBe(drawDomScale9Shape);
29
+ });
30
+ });
31
+
32
+ describe('drawDomScale9Shape', () => {
33
+ it('returns early when commands are empty', () => {
34
+ const state = createDomRenderState(document.createElement('div'));
35
+ const shape = createScale9Shape(grid);
36
+ const data = getOrCreateRenderProxy2D(state, shape);
37
+ data.rendererData = createDomScale9ShapeData(state, shape);
38
+
39
+ drawDomScale9Shape(state, data);
40
+
41
+ expect(getCurrentElement(state)).toBeNull();
42
+ });
43
+
44
+ it('returns early when rendererData is null', () => {
45
+ const state = createDomRenderState(document.createElement('div'));
46
+ const shape = createScale9Shape(grid);
47
+ appendShapeBeginFill(shape, 0xff0000);
48
+ appendShapeRectangle(shape, 0, 0, 100, 100);
49
+ const data = getOrCreateRenderProxy2D(state, shape);
50
+
51
+ drawDomScale9Shape(state, data);
52
+
53
+ expect(getCurrentElement(state)).toBeNull();
54
+ });
55
+
56
+ it('renders remapped commands into a canvas and strips object scale from the transform', () => {
57
+ const state = createDomRenderState(document.createElement('div'));
58
+ const shape = createScale9Shape(grid);
59
+ shape.scaleX = 2;
60
+ shape.scaleY = 3;
61
+ appendShapeBeginFill(shape, 0xff0000);
62
+ appendShapeRectangle(shape, 0, 0, 100, 100);
63
+ const data = getOrCreateRenderProxy2D(state, shape);
64
+ data.rendererData = createDomScale9ShapeData(state, shape);
65
+ data.transform2D.a = 2;
66
+ data.transform2D.d = 3;
67
+
68
+ drawDomScale9Shape(state, data);
69
+
70
+ const element = getCurrentElement(state) as HTMLCanvasElement;
71
+ expect(element.tagName).toBe('CANVAS');
72
+ expect(element.width).toBe(200);
73
+ expect(element.height).toBe(300);
74
+ expect(element.style.transform).toBe('matrix(1,0,0,1,0,0)');
75
+ });
76
+ });