@407dev/cms-astro 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 (212) hide show
  1. package/README.md +69 -0
  2. package/dist/chunk-2TKIQIOE.js +118 -0
  3. package/dist/chunk-2TKIQIOE.js.map +1 -0
  4. package/dist/chunk-53OQ5RXJ.js +912 -0
  5. package/dist/chunk-53OQ5RXJ.js.map +1 -0
  6. package/dist/chunk-56OMNQLH.js +1013 -0
  7. package/dist/chunk-56OMNQLH.js.map +1 -0
  8. package/dist/chunk-5AOOMQ7K.js +1031 -0
  9. package/dist/chunk-5AOOMQ7K.js.map +1 -0
  10. package/dist/chunk-5XRUCFDC.js +743 -0
  11. package/dist/chunk-5XRUCFDC.js.map +1 -0
  12. package/dist/chunk-62ZHCOLZ.js +25 -0
  13. package/dist/chunk-62ZHCOLZ.js.map +1 -0
  14. package/dist/chunk-6AZLRSYA.js +126 -0
  15. package/dist/chunk-6AZLRSYA.js.map +1 -0
  16. package/dist/chunk-6L3ZTCFB.js +14 -0
  17. package/dist/chunk-6L3ZTCFB.js.map +1 -0
  18. package/dist/chunk-6O7ZNNDT.js +697 -0
  19. package/dist/chunk-6O7ZNNDT.js.map +1 -0
  20. package/dist/chunk-6PQIXTMK.js +697 -0
  21. package/dist/chunk-6PQIXTMK.js.map +1 -0
  22. package/dist/chunk-7AW6M7QW.js +1013 -0
  23. package/dist/chunk-7AW6M7QW.js.map +1 -0
  24. package/dist/chunk-7E3XJLKX.js +73 -0
  25. package/dist/chunk-7E3XJLKX.js.map +1 -0
  26. package/dist/chunk-7LM54OTA.js +1031 -0
  27. package/dist/chunk-7LM54OTA.js.map +1 -0
  28. package/dist/chunk-7LMUFCWX.js +126 -0
  29. package/dist/chunk-7LMUFCWX.js.map +1 -0
  30. package/dist/chunk-A3OGL4OX.js +706 -0
  31. package/dist/chunk-A3OGL4OX.js.map +1 -0
  32. package/dist/chunk-AO4UAWOV.js +1030 -0
  33. package/dist/chunk-AO4UAWOV.js.map +1 -0
  34. package/dist/chunk-B4YYLS7Y.js +14 -0
  35. package/dist/chunk-B4YYLS7Y.js.map +1 -0
  36. package/dist/chunk-BDFVGXM2.js +697 -0
  37. package/dist/chunk-BDFVGXM2.js.map +1 -0
  38. package/dist/chunk-CXM7NZVX.js +1031 -0
  39. package/dist/chunk-CXM7NZVX.js.map +1 -0
  40. package/dist/chunk-DMLOBKYE.js +96 -0
  41. package/dist/chunk-DMLOBKYE.js.map +1 -0
  42. package/dist/chunk-DXJSYGS6.js +743 -0
  43. package/dist/chunk-DXJSYGS6.js.map +1 -0
  44. package/dist/chunk-EFUSAQZM.js +72 -0
  45. package/dist/chunk-EFUSAQZM.js.map +1 -0
  46. package/dist/chunk-F7KDGPBN.js +1013 -0
  47. package/dist/chunk-F7KDGPBN.js.map +1 -0
  48. package/dist/chunk-FBSQT45X.js +126 -0
  49. package/dist/chunk-FBSQT45X.js.map +1 -0
  50. package/dist/chunk-FEPDOMVH.js +697 -0
  51. package/dist/chunk-FEPDOMVH.js.map +1 -0
  52. package/dist/chunk-FQDOL2QW.js +25 -0
  53. package/dist/chunk-FQDOL2QW.js.map +1 -0
  54. package/dist/chunk-GB3LGQJM.js +109 -0
  55. package/dist/chunk-GB3LGQJM.js.map +1 -0
  56. package/dist/chunk-HCNP6R3P.js +736 -0
  57. package/dist/chunk-HCNP6R3P.js.map +1 -0
  58. package/dist/chunk-J6WEJDRI.js +743 -0
  59. package/dist/chunk-J6WEJDRI.js.map +1 -0
  60. package/dist/chunk-JHEAPNJ2.js +117 -0
  61. package/dist/chunk-JHEAPNJ2.js.map +1 -0
  62. package/dist/chunk-JOJDJ6GW.js +25 -0
  63. package/dist/chunk-JOJDJ6GW.js.map +1 -0
  64. package/dist/chunk-KTDV6WYB.js +202 -0
  65. package/dist/chunk-KTDV6WYB.js.map +1 -0
  66. package/dist/chunk-KZWJBKPC.js +734 -0
  67. package/dist/chunk-KZWJBKPC.js.map +1 -0
  68. package/dist/chunk-LEAQBGL6.js +109 -0
  69. package/dist/chunk-LEAQBGL6.js.map +1 -0
  70. package/dist/chunk-LGZJL2Q7.js +697 -0
  71. package/dist/chunk-LGZJL2Q7.js.map +1 -0
  72. package/dist/chunk-LQPBU46R.js +109 -0
  73. package/dist/chunk-LQPBU46R.js.map +1 -0
  74. package/dist/chunk-M55NNUTY.js +1013 -0
  75. package/dist/chunk-M55NNUTY.js.map +1 -0
  76. package/dist/chunk-M6NCYCC3.js +1013 -0
  77. package/dist/chunk-M6NCYCC3.js.map +1 -0
  78. package/dist/chunk-MGN7OZAU.js +109 -0
  79. package/dist/chunk-MGN7OZAU.js.map +1 -0
  80. package/dist/chunk-MGYZOX4K.js +1013 -0
  81. package/dist/chunk-MGYZOX4K.js.map +1 -0
  82. package/dist/chunk-MY4IEKJS.js +1031 -0
  83. package/dist/chunk-MY4IEKJS.js.map +1 -0
  84. package/dist/chunk-NCQRGAB4.js +705 -0
  85. package/dist/chunk-NCQRGAB4.js.map +1 -0
  86. package/dist/chunk-NI4NHXI4.js +697 -0
  87. package/dist/chunk-NI4NHXI4.js.map +1 -0
  88. package/dist/chunk-NPI5N7AU.js +14 -0
  89. package/dist/chunk-NPI5N7AU.js.map +1 -0
  90. package/dist/chunk-QN4CPMJF.js +126 -0
  91. package/dist/chunk-QN4CPMJF.js.map +1 -0
  92. package/dist/chunk-QYUVAKMC.js +1013 -0
  93. package/dist/chunk-QYUVAKMC.js.map +1 -0
  94. package/dist/chunk-RAAT6PDT.js +25 -0
  95. package/dist/chunk-RAAT6PDT.js.map +1 -0
  96. package/dist/chunk-RDPO5VPS.js +1013 -0
  97. package/dist/chunk-RDPO5VPS.js.map +1 -0
  98. package/dist/chunk-RQHIW2PF.js +25 -0
  99. package/dist/chunk-RQHIW2PF.js.map +1 -0
  100. package/dist/chunk-RREVHA52.js +722 -0
  101. package/dist/chunk-RREVHA52.js.map +1 -0
  102. package/dist/chunk-S7PT4PZN.js +25 -0
  103. package/dist/chunk-S7PT4PZN.js.map +1 -0
  104. package/dist/chunk-SUXV5M32.js +109 -0
  105. package/dist/chunk-SUXV5M32.js.map +1 -0
  106. package/dist/chunk-SZEORUY6.js +696 -0
  107. package/dist/chunk-SZEORUY6.js.map +1 -0
  108. package/dist/chunk-T7NGGWGY.js +25 -0
  109. package/dist/chunk-T7NGGWGY.js.map +1 -0
  110. package/dist/chunk-UEVWP5VE.js +696 -0
  111. package/dist/chunk-UEVWP5VE.js.map +1 -0
  112. package/dist/chunk-UJNKNP7U.js +748 -0
  113. package/dist/chunk-UJNKNP7U.js.map +1 -0
  114. package/dist/chunk-V4XVFWFI.js +1013 -0
  115. package/dist/chunk-V4XVFWFI.js.map +1 -0
  116. package/dist/chunk-VJU2CIKZ.js +729 -0
  117. package/dist/chunk-VJU2CIKZ.js.map +1 -0
  118. package/dist/chunk-VRQUP5L3.js +202 -0
  119. package/dist/chunk-VRQUP5L3.js.map +1 -0
  120. package/dist/chunk-WBJ5E7CL.js +742 -0
  121. package/dist/chunk-WBJ5E7CL.js.map +1 -0
  122. package/dist/chunk-XJPVYMZM.js +24 -0
  123. package/dist/chunk-XJPVYMZM.js.map +1 -0
  124. package/dist/chunk-Y3Q65C7R.js +14 -0
  125. package/dist/chunk-Y3Q65C7R.js.map +1 -0
  126. package/dist/chunk-YHEGFPTR.js +1030 -0
  127. package/dist/chunk-YHEGFPTR.js.map +1 -0
  128. package/dist/chunk-ZDLBN46C.js +697 -0
  129. package/dist/chunk-ZDLBN46C.js.map +1 -0
  130. package/dist/dev/index.d.ts +122 -0
  131. package/dist/dev/index.js +662 -0
  132. package/dist/dev/index.js.map +1 -0
  133. package/dist/fields/collections.d.ts +18 -0
  134. package/dist/fields/collections.js +37 -0
  135. package/dist/fields/collections.js.map +1 -0
  136. package/dist/index.d.ts +235 -0
  137. package/dist/index.js +956 -0
  138. package/dist/index.js.map +1 -0
  139. package/dist/middleware/index.d.ts +8 -0
  140. package/dist/middleware/index.js +13 -0
  141. package/dist/middleware/index.js.map +1 -0
  142. package/dist/preview-BEkLMD6N.d.ts +23 -0
  143. package/dist/preview-D9yewK8x.d.ts +24 -0
  144. package/dist/preview-DJEA4fj7.d.ts +23 -0
  145. package/dist/preview-DeD1Jn7U.d.ts +23 -0
  146. package/dist/toolbar/app.d.ts +28 -0
  147. package/dist/toolbar/app.js +1176 -0
  148. package/dist/toolbar/app.js.map +1 -0
  149. package/dist/types-15jfCQKb.d.ts +141 -0
  150. package/dist/types-BREdL9Gx.d.ts +140 -0
  151. package/dist/types-C6sUZNa2.d.ts +104 -0
  152. package/dist/types-CEYS5Fzc.d.ts +112 -0
  153. package/dist/types-DpeHpobR.d.ts +104 -0
  154. package/dist/types-NaKJe6Pg.d.ts +140 -0
  155. package/package.json +90 -0
  156. package/src/CmsHead.astro +25 -0
  157. package/src/CmsImage.astro +62 -0
  158. package/src/CmsVideo.astro +185 -0
  159. package/src/RichText.astro +32 -0
  160. package/src/client.test.ts +227 -0
  161. package/src/client.ts +164 -0
  162. package/src/dev/config.test.ts +258 -0
  163. package/src/dev/config.ts +319 -0
  164. package/src/dev/index.ts +5 -0
  165. package/src/dev/preview-token.ts +39 -0
  166. package/src/dev/routes.test.ts +354 -0
  167. package/src/dev/routes.ts +245 -0
  168. package/src/dev/token.test.ts +280 -0
  169. package/src/dev/token.ts +201 -0
  170. package/src/dev/verifier.test.ts +63 -0
  171. package/src/dev/verifier.ts +161 -0
  172. package/src/fields/attrs.ts +24 -0
  173. package/src/fields/collection.ts +72 -0
  174. package/src/fields/collections.ts +35 -0
  175. package/src/fields/entryScope.test.ts +62 -0
  176. package/src/fields/entryScope.ts +240 -0
  177. package/src/fields/group.ts +51 -0
  178. package/src/fields/helpers.test.ts +51 -0
  179. package/src/fields/helpers.ts +214 -0
  180. package/src/fields/index.ts +46 -0
  181. package/src/fields/scope.ts +21 -0
  182. package/src/fields/types.ts +119 -0
  183. package/src/head.test.ts +74 -0
  184. package/src/head.ts +123 -0
  185. package/src/image.test.ts +42 -0
  186. package/src/image.ts +64 -0
  187. package/src/index.test.ts +76 -0
  188. package/src/index.ts +13 -0
  189. package/src/integration.ts +218 -0
  190. package/src/jwt.ts +131 -0
  191. package/src/middleware/index.ts +4 -0
  192. package/src/middleware/preview.test.ts +184 -0
  193. package/src/middleware/preview.ts +206 -0
  194. package/src/preview/bridge-script.test.ts +683 -0
  195. package/src/preview/bridge-script.ts +763 -0
  196. package/src/preview/origins.ts +29 -0
  197. package/src/preview.ts +49 -0
  198. package/src/richtext/RichTextNode.astro +130 -0
  199. package/src/richtext/render.ts +248 -0
  200. package/src/richtext.test.ts +139 -0
  201. package/src/runtime/bundle.ts +28 -0
  202. package/src/runtime/store.ts +130 -0
  203. package/src/siteFiles.test.ts +93 -0
  204. package/src/siteFiles.ts +111 -0
  205. package/src/toolbar/app.test.ts +191 -0
  206. package/src/toolbar/app.ts +1091 -0
  207. package/src/toolbar/icon.ts +5 -0
  208. package/src/toolbar/widgets.test.ts +153 -0
  209. package/src/toolbar/widgets.ts +365 -0
  210. package/src/types.test.ts +154 -0
  211. package/src/types.ts +163 -0
  212. package/src/version.ts +3 -0
@@ -0,0 +1,683 @@
1
+ // @vitest-environment jsdom
2
+ import { PREVIEW_BRIDGE_PROTOCOL_VERSION } from '@407dev/api-client';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { BRIDGE_PROTOCOL_VERSION, buildBridgeScript } from './bridge-script';
5
+
6
+ describe('bridge-script protocol parity', () => {
7
+ it('matches PREVIEW_BRIDGE_PROTOCOL_VERSION exactly', () => {
8
+ expect(BRIDGE_PROTOCOL_VERSION).toBe(PREVIEW_BRIDGE_PROTOCOL_VERSION);
9
+ });
10
+
11
+ it('builds a self-contained IIFE with default or custom origins', () => {
12
+ const scriptDefault = buildBridgeScript();
13
+ expect(scriptDefault).toContain('https://web.localhost');
14
+ expect(scriptDefault).toContain('http://localhost:5173');
15
+ expect(scriptDefault).toContain('window.__CMS_BRIDGE_INITIALIZED__');
16
+
17
+ const scriptCustom = buildBridgeScript({
18
+ editorOrigins: ['https://studio.example.com', 'http://localhost:3000'],
19
+ });
20
+ expect(scriptCustom).toContain('https://studio.example.com');
21
+ expect(scriptCustom).toContain('http://localhost:3000');
22
+ });
23
+
24
+ it('contains handlers for all preview-to-editor message types', () => {
25
+ const script = buildBridgeScript();
26
+ // Emitted message types
27
+ expect(script).toContain("type: 'ready'");
28
+ expect(script).toContain("type: 'field-click'");
29
+ expect(script).toContain("type: 'group-click'");
30
+ expect(script).toContain("type: 'rects-update'");
31
+ expect(script).toContain("type: 'rendered-fields'");
32
+
33
+ // Handled editor message types
34
+ expect(script).toContain("data.type === 'hello'");
35
+ expect(script).toContain("data.type === 'highlight-mode'");
36
+ expect(script).toContain("data.type === 'patch'");
37
+ expect(script).toContain("data.type === 'scroll-to'");
38
+ expect(script).toContain("data.type === 'reload'");
39
+
40
+ // Verifies entry-scoped selector logic is present
41
+ expect(script).toContain('data-cms-collection');
42
+ expect(script).toContain('data-cms-entry');
43
+ });
44
+
45
+ it('writes rich record array to __CMS_FIELDS__, exposes __CMS_RESCAN__, and dispatches cms:fields-updated', () => {
46
+ document.body.innerHTML = `
47
+ <div data-cms-group="Hero:home.hero">
48
+ <h1 data-cms-field="home.hero.heading">Welcome</h1>
49
+ <p data-cms-field="home.hero.subheading">Subheading text</p>
50
+ </div>
51
+ <div data-cms-collection="blog" data-cms-entry="hello-world">
52
+ <h2 data-cms-entry-field="title">Hello World</h2>
53
+ </div>
54
+ `;
55
+
56
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
57
+ false;
58
+
59
+ const fieldsUpdatedListener = vi.fn();
60
+ document.addEventListener('cms:fields-updated', fieldsUpdatedListener);
61
+
62
+ const scriptCode = buildBridgeScript();
63
+ const runScript = new Function(scriptCode);
64
+ runScript();
65
+
66
+ const win = window as unknown as {
67
+ __CMS_FIELDS__?: Array<{
68
+ key: string;
69
+ kind: string;
70
+ group?: string;
71
+ entryRef?: { collection: string; slug: string; path: string };
72
+ instances: number;
73
+ }>;
74
+ __CMS_RESCAN__?: () => void;
75
+ };
76
+
77
+ expect(Array.isArray(win.__CMS_FIELDS__)).toBe(true);
78
+ expect(win.__CMS_FIELDS__?.length).toBe(3);
79
+
80
+ const heroHeading = win.__CMS_FIELDS__?.find((f) => f.key === 'home.hero.heading');
81
+ expect(heroHeading).toBeDefined();
82
+ expect(heroHeading?.group).toBe('Hero');
83
+ expect(heroHeading?.kind).toBe('text');
84
+
85
+ const blogTitle = win.__CMS_FIELDS__?.find((f) => f.key === 'title');
86
+ expect(blogTitle).toBeDefined();
87
+ expect(blogTitle?.entryRef).toEqual({
88
+ collection: 'blog',
89
+ slug: 'hello-world',
90
+ path: 'title',
91
+ });
92
+
93
+ expect(typeof win.__CMS_RESCAN__).toBe('function');
94
+ expect(fieldsUpdatedListener).toHaveBeenCalled();
95
+ });
96
+
97
+ it('scopes patch updates to specific entry without contaminating other entries sharing field keys', () => {
98
+ // Set up a mock DOM environment
99
+ document.body.innerHTML = `
100
+ <div data-cms-collection="blog" data-cms-entry="post-1">
101
+ <h2 data-cms-entry-field="title">Original Title 1</h2>
102
+ <span data-cms-entry-field="excerpt">Excerpt 1</span>
103
+ </div>
104
+ <div data-cms-collection="blog" data-cms-entry="post-2">
105
+ <h2 data-cms-entry-field="title">Original Title 2</h2>
106
+ <span data-cms-entry-field="excerpt">Excerpt 2</span>
107
+ </div>
108
+ <h1 data-cms-field="site.title">Global Site Title</h1>
109
+ `;
110
+
111
+ // Initialize the bridge script in window
112
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
113
+ false;
114
+ const scriptCode = buildBridgeScript({ editorOrigins: ['http://localhost:5173'] });
115
+ const runScript = new Function(scriptCode);
116
+ runScript();
117
+
118
+ // 1. Establish connection via hello message
119
+ window.dispatchEvent(
120
+ new MessageEvent('message', {
121
+ origin: 'http://localhost:5173',
122
+ source: window,
123
+ data: {
124
+ source: 'cms-editor',
125
+ v: PREVIEW_BRIDGE_PROTOCOL_VERSION,
126
+ type: 'hello',
127
+ protocolVersion: PREVIEW_BRIDGE_PROTOCOL_VERSION,
128
+ },
129
+ }),
130
+ );
131
+
132
+ // 2. Dispatch patch for post-1 title
133
+ window.dispatchEvent(
134
+ new MessageEvent('message', {
135
+ origin: 'http://localhost:5173',
136
+ source: window,
137
+ data: {
138
+ source: 'cms-editor',
139
+ v: PREVIEW_BRIDGE_PROTOCOL_VERSION,
140
+ type: 'patch',
141
+ key: 'title',
142
+ kind: 'text',
143
+ value: 'Patched Title 1 Only',
144
+ entryRef: {
145
+ collection: 'blog',
146
+ slug: 'post-1',
147
+ path: 'title',
148
+ },
149
+ },
150
+ }),
151
+ );
152
+
153
+ const post1Title = document.querySelector(
154
+ '[data-cms-collection="blog"][data-cms-entry="post-1"] [data-cms-entry-field="title"]',
155
+ );
156
+ const post2Title = document.querySelector(
157
+ '[data-cms-collection="blog"][data-cms-entry="post-2"] [data-cms-entry-field="title"]',
158
+ );
159
+
160
+ expect(post1Title?.textContent).toBe('Patched Title 1 Only');
161
+ expect(post2Title?.textContent).toBe('Original Title 2');
162
+
163
+ // 3. Dispatch patch for global site field
164
+ window.dispatchEvent(
165
+ new MessageEvent('message', {
166
+ origin: 'http://localhost:5173',
167
+ source: window,
168
+ data: {
169
+ source: 'cms-editor',
170
+ v: PREVIEW_BRIDGE_PROTOCOL_VERSION,
171
+ type: 'patch',
172
+ key: 'site.title',
173
+ kind: 'text',
174
+ value: 'Patched Global Site Title',
175
+ },
176
+ }),
177
+ );
178
+
179
+ const globalTitle = document.querySelector('[data-cms-field="site.title"]');
180
+ expect(globalTitle?.textContent).toBe('Patched Global Site Title');
181
+ });
182
+
183
+ it('dispatches cms:field-selected CustomEvent on document when field element is clicked in edit mode', () => {
184
+ document.body.innerHTML = `
185
+ <h1 data-cms-field="home.hero.heading">Click Me</h1>
186
+ `;
187
+ document.documentElement.setAttribute('data-cms-edit-mode', 'true');
188
+
189
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
190
+ false;
191
+
192
+ const selectedListener = vi.fn();
193
+ document.addEventListener('cms:field-selected', selectedListener);
194
+
195
+ const scriptCode = buildBridgeScript();
196
+ const runScript = new Function(scriptCode);
197
+ runScript();
198
+
199
+ const heading = document.querySelector('[data-cms-field="home.hero.heading"]');
200
+ heading?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
201
+
202
+ expect(selectedListener).toHaveBeenCalled();
203
+ const customEvent = selectedListener.mock.calls[0][0] as CustomEvent;
204
+ expect(customEvent.detail.key).toBe('home.hero.heading');
205
+ });
206
+
207
+ it('skips textContent replacement when patching structured non-image object values', () => {
208
+ document.body.innerHTML = `
209
+ <div data-cms-field="home.videoFeed.videos">
210
+ <video src="/video1.mp4"></video>
211
+ </div>
212
+ `;
213
+
214
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
215
+ false;
216
+ const scriptCode = buildBridgeScript({ editorOrigins: ['http://localhost:5173'] });
217
+ const runScript = new Function(scriptCode);
218
+ runScript();
219
+
220
+ // 1. Establish connection via hello message
221
+ window.dispatchEvent(
222
+ new MessageEvent('message', {
223
+ origin: 'http://localhost:5173',
224
+ source: window,
225
+ data: {
226
+ source: 'cms-editor',
227
+ v: PREVIEW_BRIDGE_PROTOCOL_VERSION,
228
+ type: 'hello',
229
+ protocolVersion: PREVIEW_BRIDGE_PROTOCOL_VERSION,
230
+ },
231
+ }),
232
+ );
233
+
234
+ // 2. Dispatch structured patch (array of video objects)
235
+ window.dispatchEvent(
236
+ new MessageEvent('message', {
237
+ origin: 'http://localhost:5173',
238
+ source: window,
239
+ data: {
240
+ source: 'cms-editor',
241
+ v: PREVIEW_BRIDGE_PROTOCOL_VERSION,
242
+ type: 'patch',
243
+ key: 'home.videoFeed.videos',
244
+ kind: 'media.videoList',
245
+ value: [{ src: '/video2.mp4' }],
246
+ },
247
+ }),
248
+ );
249
+
250
+ const videoEl = document.querySelector('[data-cms-field="home.videoFeed.videos"]');
251
+ // Ensure textContent was NOT set to "[object Object]"
252
+ expect(videoEl?.textContent).not.toContain('[object Object]');
253
+ expect(videoEl?.querySelector('video')).not.toBeNull();
254
+ });
255
+
256
+ it('hides astro-dev-toolbar when embedded in an iframe', () => {
257
+ const iframe = document.createElement('iframe');
258
+ document.body.appendChild(iframe);
259
+
260
+ const iframeDoc = iframe.contentDocument;
261
+ const iframeWin = iframe.contentWindow as Window & typeof globalThis;
262
+ expect(iframeWin).toBeDefined();
263
+
264
+ const scriptCode = buildBridgeScript();
265
+ const runScript = new iframeWin.Function(scriptCode);
266
+ runScript();
267
+
268
+ const injectedStyle = iframeDoc?.getElementById('cms-bridge-styles');
269
+ expect(injectedStyle).not.toBeNull();
270
+ expect(injectedStyle?.textContent).toContain('astro-dev-toolbar { display: none !important; }');
271
+
272
+ iframe.remove();
273
+ });
274
+
275
+ it('restores toolbar suppression after page swap (astro:after-swap / astro:page-load)', () => {
276
+ const iframe = document.createElement('iframe');
277
+ document.body.appendChild(iframe);
278
+
279
+ const iframeDoc = iframe.contentDocument;
280
+ const iframeWin = iframe.contentWindow as Window & typeof globalThis;
281
+ expect(iframeWin).toBeDefined();
282
+
283
+ const scriptCode = buildBridgeScript();
284
+ const runScript = new iframeWin.Function(scriptCode);
285
+ runScript();
286
+
287
+ expect(iframeDoc?.getElementById('cms-bridge-styles')).not.toBeNull();
288
+
289
+ // Simulate Astro ViewTransitions swap removing head styles
290
+ const styleEl = iframeDoc?.getElementById('cms-bridge-styles');
291
+ styleEl?.remove();
292
+ expect(iframeDoc?.getElementById('cms-bridge-styles')).toBeNull();
293
+
294
+ // Simulate toolbar element added to new body
295
+ const toolbar = iframeDoc?.createElement('astro-dev-toolbar');
296
+ if (toolbar && iframeDoc?.body) {
297
+ iframeDoc.body.appendChild(toolbar);
298
+ }
299
+
300
+ // Trigger after-swap event
301
+ iframeDoc?.dispatchEvent(new Event('astro:after-swap'));
302
+
303
+ const reInjectedStyle = iframeDoc?.getElementById('cms-bridge-styles');
304
+ expect(reInjectedStyle).not.toBeNull();
305
+ expect(reInjectedStyle?.textContent).toContain(
306
+ 'astro-dev-toolbar { display: none !important; }',
307
+ );
308
+ expect(toolbar?.style.display).toBe('none');
309
+
310
+ iframe.remove();
311
+ });
312
+
313
+ it('does not hide astro-dev-toolbar when loaded top-level', () => {
314
+ const oldStyle = document.getElementById('cms-bridge-styles');
315
+ if (oldStyle) oldStyle.remove();
316
+
317
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
318
+ false;
319
+
320
+ const scriptCode = buildBridgeScript();
321
+ const runScript = new Function(scriptCode);
322
+ runScript();
323
+
324
+ const injectedStyle = document.getElementById('cms-bridge-styles');
325
+ expect(injectedStyle).not.toBeNull();
326
+ expect(injectedStyle?.textContent).not.toContain('astro-dev-toolbar');
327
+ });
328
+
329
+ it('includes comments capability and handles comment-mode messages', () => {
330
+ const script = buildBridgeScript();
331
+ expect(script).toContain("capabilities: ['comments']");
332
+ expect(script).toContain("data.type === 'comment-mode'");
333
+ expect(script).toContain("data.type === 'watch-anchors'");
334
+ expect(script).toContain("data.type === 'scroll-to-anchor'");
335
+ expect(script).toContain("type: 'comment-click'");
336
+ expect(script).toContain("type: 'anchor-rects'");
337
+ });
338
+
339
+ it('suppresses default clicks and emits comment-click with full anchor chain in comment mode', () => {
340
+ document.body.innerHTML = `
341
+ <div data-cms-group="Hero:home.hero">
342
+ <a href="https://example.com" id="test-link">
343
+ <h1 data-cms-field="home.hero.heading" id="test-heading">Welcome to Site</h1>
344
+ </a>
345
+ </div>
346
+ `;
347
+
348
+ document.documentElement.setAttribute('data-cms-comment-mode', 'true');
349
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
350
+ false;
351
+
352
+ const scriptCode = buildBridgeScript();
353
+ const runScript = new Function(scriptCode);
354
+ runScript();
355
+
356
+ // Mock parent postMessage
357
+ const messages: unknown[] = [];
358
+ const mockParent = {
359
+ postMessage: (msg: unknown) => messages.push(msg),
360
+ };
361
+
362
+ // Simulate hello handshake to connect origin
363
+ window.dispatchEvent(
364
+ new MessageEvent('message', {
365
+ data: { source: 'cms-editor', v: 1, type: 'hello', protocolVersion: 1 },
366
+ origin: 'https://web.localhost',
367
+ source: mockParent as unknown as MessageEventSource,
368
+ }),
369
+ );
370
+
371
+ const heading = document.getElementById('test-heading');
372
+ expect(heading).toBeDefined();
373
+ const clickEvent = new MouseEvent('click', {
374
+ bubbles: true,
375
+ cancelable: true,
376
+ clientX: 100,
377
+ clientY: 150,
378
+ });
379
+
380
+ const defaultPrevented = heading ? !heading.dispatchEvent(clickEvent) : false;
381
+ expect(defaultPrevented).toBe(true);
382
+
383
+ const commentClickMsg = messages.find(
384
+ (m) => (m as { type?: string }).type === 'comment-click',
385
+ ) as
386
+ | {
387
+ type: string;
388
+ chain: Array<{ selector: string; kind: string; label: string; instance: number }>;
389
+ point: { pageXPct: number; pageYPct: number };
390
+ }
391
+ | undefined;
392
+
393
+ expect(commentClickMsg).toBeDefined();
394
+ expect(commentClickMsg?.chain.length).toBe(2);
395
+ expect(commentClickMsg?.chain[0]).toMatchObject({
396
+ selector: '[data-cms-field="home.hero.heading"]',
397
+ kind: 'text',
398
+ label: 'home.hero.heading',
399
+ instance: 0,
400
+ });
401
+ expect(commentClickMsg?.chain[1]).toMatchObject({
402
+ selector: '[data-cms-group="Hero:home.hero"]',
403
+ kind: 'group',
404
+ label: 'Hero',
405
+ instance: 0,
406
+ });
407
+
408
+ document.documentElement.removeAttribute('data-cms-comment-mode');
409
+ });
410
+
411
+ it('resolves orphan anchors to null in anchor-rects message', async () => {
412
+ document.body.innerHTML = `
413
+ <div data-cms-field="existing.field">Existing</div>
414
+ `;
415
+
416
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
417
+ false;
418
+
419
+ const scriptCode = buildBridgeScript();
420
+ const runScript = new Function(scriptCode);
421
+ runScript();
422
+
423
+ const messages: unknown[] = [];
424
+ const mockParent = {
425
+ postMessage: (msg: unknown) => messages.push(msg),
426
+ };
427
+
428
+ window.dispatchEvent(
429
+ new MessageEvent('message', {
430
+ data: { source: 'cms-editor', v: 1, type: 'hello', protocolVersion: 1 },
431
+ origin: 'https://web.localhost',
432
+ source: mockParent as unknown as MessageEventSource,
433
+ }),
434
+ );
435
+
436
+ // Watch both an existing anchor and an orphaned anchor
437
+ window.dispatchEvent(
438
+ new MessageEvent('message', {
439
+ data: {
440
+ source: 'cms-editor',
441
+ v: 1,
442
+ type: 'watch-anchors',
443
+ anchors: [
444
+ { id: 'comment-1', selector: '[data-cms-field="existing.field"]', instance: 0 },
445
+ { id: 'comment-orphan', selector: '[data-cms-field="deleted.field"]', instance: 0 },
446
+ ],
447
+ },
448
+ origin: 'https://web.localhost',
449
+ source: mockParent as unknown as MessageEventSource,
450
+ }),
451
+ );
452
+
453
+ // Allow requestAnimationFrame to run
454
+ await new Promise((resolve) => requestAnimationFrame(resolve));
455
+
456
+ const anchorRectsMsg = messages.find((m) => (m as { type?: string }).type === 'anchor-rects') as
457
+ | {
458
+ type: string;
459
+ rects: Record<string, unknown>;
460
+ }
461
+ | undefined;
462
+
463
+ expect(anchorRectsMsg).toBeDefined();
464
+ expect(anchorRectsMsg?.rects['comment-1']).not.toBeNull();
465
+ expect(anchorRectsMsg?.rects['comment-orphan']).toBeNull();
466
+ });
467
+
468
+ it('emits comment-click with generic structural anchor chain on elements without data-cms-* ancestors', () => {
469
+ document.body.innerHTML = `
470
+ <nav class="site-nav">
471
+ <a href="/about">About Us</a>
472
+ </nav>
473
+ `;
474
+
475
+ document.documentElement.setAttribute('data-cms-comment-mode', 'true');
476
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
477
+ false;
478
+
479
+ const scriptCode = buildBridgeScript();
480
+ const runScript = new Function(scriptCode);
481
+ runScript();
482
+
483
+ const messages: unknown[] = [];
484
+ const mockParent = {
485
+ postMessage: (msg: unknown) => messages.push(msg),
486
+ };
487
+
488
+ window.dispatchEvent(
489
+ new MessageEvent('message', {
490
+ data: { source: 'cms-editor', v: 1, type: 'hello', protocolVersion: 1 },
491
+ origin: 'https://web.localhost',
492
+ source: mockParent as unknown as MessageEventSource,
493
+ }),
494
+ );
495
+
496
+ const link = document.querySelector('a');
497
+ expect(link).toBeDefined();
498
+ const clickEvent = new MouseEvent('click', {
499
+ bubbles: true,
500
+ cancelable: true,
501
+ clientX: 50,
502
+ clientY: 60,
503
+ });
504
+
505
+ const defaultPrevented = link ? !link.dispatchEvent(clickEvent) : false;
506
+ expect(defaultPrevented).toBe(true);
507
+
508
+ const commentClickMsg = messages.find(
509
+ (m) => (m as { type?: string }).type === 'comment-click',
510
+ ) as
511
+ | {
512
+ type: string;
513
+ chain: Array<{
514
+ selector: string;
515
+ kind: string;
516
+ label: string;
517
+ instance: number;
518
+ rect: { top: number; left: number; width: number; height: number };
519
+ }>;
520
+ point: { pageXPct: number; pageYPct: number };
521
+ rect: { top: number; left: number; width: number; height: number };
522
+ }
523
+ | undefined;
524
+
525
+ expect(commentClickMsg).toBeDefined();
526
+ expect(commentClickMsg?.chain.length).toBe(2);
527
+ expect(commentClickMsg?.chain[0]).toMatchObject({
528
+ selector: 'body > nav:nth-of-type(1) > a:nth-of-type(1)',
529
+ kind: 'element',
530
+ label: 'About Us',
531
+ instance: 0,
532
+ });
533
+ expect(commentClickMsg?.chain[1]).toMatchObject({
534
+ selector: 'body > nav:nth-of-type(1)',
535
+ kind: 'element',
536
+ label: 'Navigation',
537
+ instance: 0,
538
+ });
539
+ expect(commentClickMsg?.rect).toBeDefined();
540
+ expect(commentClickMsg?.point).toBeDefined();
541
+
542
+ document.documentElement.removeAttribute('data-cms-comment-mode');
543
+ });
544
+
545
+ it('resolves generic structural anchors to non-null rects in anchor-rects message', async () => {
546
+ document.body.innerHTML = `
547
+ <header id="site-header">
548
+ <nav>
549
+ <a href="/pricing">Pricing</a>
550
+ </nav>
551
+ </header>
552
+ `;
553
+
554
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
555
+ false;
556
+
557
+ const scriptCode = buildBridgeScript();
558
+ const runScript = new Function(scriptCode);
559
+ runScript();
560
+
561
+ const messages: unknown[] = [];
562
+ const mockParent = {
563
+ postMessage: (msg: unknown) => messages.push(msg),
564
+ };
565
+
566
+ window.dispatchEvent(
567
+ new MessageEvent('message', {
568
+ data: { source: 'cms-editor', v: 1, type: 'hello', protocolVersion: 1 },
569
+ origin: 'https://web.localhost',
570
+ source: mockParent as unknown as MessageEventSource,
571
+ }),
572
+ );
573
+
574
+ window.dispatchEvent(
575
+ new MessageEvent('message', {
576
+ data: {
577
+ source: 'cms-editor',
578
+ v: 1,
579
+ type: 'watch-anchors',
580
+ anchors: [
581
+ {
582
+ id: 'comment-generic-link',
583
+ selector: '#site-header > nav:nth-of-type(1) > a:nth-of-type(1)',
584
+ instance: 0,
585
+ },
586
+ { id: 'comment-generic-header', selector: '#site-header', instance: 0 },
587
+ {
588
+ id: 'comment-generic-orphan',
589
+ selector: '#site-header > footer:nth-of-type(1)',
590
+ instance: 0,
591
+ },
592
+ ],
593
+ },
594
+ origin: 'https://web.localhost',
595
+ source: mockParent as unknown as MessageEventSource,
596
+ }),
597
+ );
598
+
599
+ await new Promise((resolve) => requestAnimationFrame(resolve));
600
+
601
+ const anchorRectsMsg = messages.find((m) => (m as { type?: string }).type === 'anchor-rects') as
602
+ | {
603
+ type: string;
604
+ rects: Record<string, unknown>;
605
+ }
606
+ | undefined;
607
+
608
+ expect(anchorRectsMsg).toBeDefined();
609
+ expect(anchorRectsMsg?.rects['comment-generic-link']).not.toBeNull();
610
+ expect(anchorRectsMsg?.rects['comment-generic-header']).not.toBeNull();
611
+ expect(anchorRectsMsg?.rects['comment-generic-orphan']).toBeNull();
612
+ });
613
+
614
+ it('anchors deep icon-in-button clicks to enclosing button and landmark', () => {
615
+ document.body.innerHTML = `
616
+ <nav id="navbar">
617
+ <button aria-label="Search site">
618
+ <svg><path id="icon-path" d="M0 0"></path></svg>
619
+ <span>Search</span>
620
+ </button>
621
+ </nav>
622
+ `;
623
+
624
+ document.documentElement.setAttribute('data-cms-comment-mode', 'true');
625
+ (window as unknown as { __CMS_BRIDGE_INITIALIZED__?: boolean }).__CMS_BRIDGE_INITIALIZED__ =
626
+ false;
627
+
628
+ const scriptCode = buildBridgeScript();
629
+ const runScript = new Function(scriptCode);
630
+ runScript();
631
+
632
+ const messages: unknown[] = [];
633
+ const mockParent = {
634
+ postMessage: (msg: unknown) => messages.push(msg),
635
+ };
636
+
637
+ window.dispatchEvent(
638
+ new MessageEvent('message', {
639
+ data: { source: 'cms-editor', v: 1, type: 'hello', protocolVersion: 1 },
640
+ origin: 'https://web.localhost',
641
+ source: mockParent as unknown as MessageEventSource,
642
+ }),
643
+ );
644
+
645
+ const path = document.getElementById('icon-path');
646
+ expect(path).toBeDefined();
647
+ const clickEvent = new MouseEvent('click', {
648
+ bubbles: true,
649
+ cancelable: true,
650
+ clientX: 20,
651
+ clientY: 30,
652
+ });
653
+
654
+ const defaultPrevented = path ? !path.dispatchEvent(clickEvent) : false;
655
+ expect(defaultPrevented).toBe(true);
656
+
657
+ const commentClickMsg = messages.find(
658
+ (m) => (m as { type?: string }).type === 'comment-click',
659
+ ) as
660
+ | {
661
+ type: string;
662
+ chain: Array<{ selector: string; kind: string; label: string; instance: number }>;
663
+ }
664
+ | undefined;
665
+
666
+ expect(commentClickMsg).toBeDefined();
667
+ expect(commentClickMsg?.chain.length).toBe(2);
668
+ expect(commentClickMsg?.chain[0]).toMatchObject({
669
+ selector: '#navbar > button:nth-of-type(1)',
670
+ kind: 'element',
671
+ label: 'Search site',
672
+ instance: 0,
673
+ });
674
+ expect(commentClickMsg?.chain[1]).toMatchObject({
675
+ selector: '#navbar',
676
+ kind: 'element',
677
+ label: '#navbar',
678
+ instance: 0,
679
+ });
680
+
681
+ document.documentElement.removeAttribute('data-cms-comment-mode');
682
+ });
683
+ });