@wordpress/core-data 7.41.2-next.v.202603102151.0 → 7.42.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 (166) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +19 -0
  3. package/build/actions.cjs +17 -25
  4. package/build/actions.cjs.map +2 -2
  5. package/build/awareness/post-editor-awareness.cjs +46 -6
  6. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  7. package/build/awareness/types.cjs.map +1 -1
  8. package/build/entities.cjs +33 -7
  9. package/build/entities.cjs.map +2 -2
  10. package/build/hooks/use-entity-prop.cjs +2 -1
  11. package/build/hooks/use-entity-prop.cjs.map +2 -2
  12. package/build/hooks/use-post-editor-awareness-state.cjs +84 -3
  13. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  14. package/build/index.cjs +3 -0
  15. package/build/index.cjs.map +2 -2
  16. package/build/private-apis.cjs +3 -1
  17. package/build/private-apis.cjs.map +2 -2
  18. package/build/queried-data/get-query-parts.cjs +7 -0
  19. package/build/queried-data/get-query-parts.cjs.map +2 -2
  20. package/build/queried-data/selectors.cjs +19 -5
  21. package/build/queried-data/selectors.cjs.map +2 -2
  22. package/build/reducer.cjs +6 -0
  23. package/build/reducer.cjs.map +2 -2
  24. package/build/resolvers.cjs +110 -74
  25. package/build/resolvers.cjs.map +2 -2
  26. package/build/selectors.cjs +29 -0
  27. package/build/selectors.cjs.map +2 -2
  28. package/build/sync.cjs +3 -0
  29. package/build/sync.cjs.map +2 -2
  30. package/build/types.cjs +16 -0
  31. package/build/types.cjs.map +3 -3
  32. package/build/utils/block-selection-history.cjs +1 -1
  33. package/build/utils/block-selection-history.cjs.map +2 -2
  34. package/build/utils/crdt-blocks.cjs +17 -3
  35. package/build/utils/crdt-blocks.cjs.map +2 -2
  36. package/build/utils/crdt-selection.cjs +4 -1
  37. package/build/utils/crdt-selection.cjs.map +2 -2
  38. package/build/utils/crdt-user-selections.cjs +9 -6
  39. package/build/utils/crdt-user-selections.cjs.map +2 -2
  40. package/build/utils/crdt-utils.cjs +54 -2
  41. package/build/utils/crdt-utils.cjs.map +2 -2
  42. package/build/utils/crdt.cjs +4 -23
  43. package/build/utils/crdt.cjs.map +2 -2
  44. package/build/utils/index.cjs +3 -0
  45. package/build/utils/index.cjs.map +2 -2
  46. package/build/utils/normalize-query-for-resolution.cjs +35 -0
  47. package/build/utils/normalize-query-for-resolution.cjs.map +7 -0
  48. package/build-module/actions.mjs +17 -25
  49. package/build-module/actions.mjs.map +2 -2
  50. package/build-module/awareness/post-editor-awareness.mjs +46 -6
  51. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  52. package/build-module/entities.mjs +33 -7
  53. package/build-module/entities.mjs.map +2 -2
  54. package/build-module/hooks/use-entity-prop.mjs +2 -1
  55. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  56. package/build-module/hooks/use-post-editor-awareness-state.mjs +81 -2
  57. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  58. package/build-module/index.mjs +2 -0
  59. package/build-module/index.mjs.map +2 -2
  60. package/build-module/private-apis.mjs +6 -2
  61. package/build-module/private-apis.mjs.map +2 -2
  62. package/build-module/queried-data/get-query-parts.mjs +7 -0
  63. package/build-module/queried-data/get-query-parts.mjs.map +2 -2
  64. package/build-module/queried-data/selectors.mjs +19 -5
  65. package/build-module/queried-data/selectors.mjs.map +2 -2
  66. package/build-module/reducer.mjs +6 -0
  67. package/build-module/reducer.mjs.map +2 -2
  68. package/build-module/resolvers.mjs +112 -75
  69. package/build-module/resolvers.mjs.map +2 -2
  70. package/build-module/selectors.mjs +28 -0
  71. package/build-module/selectors.mjs.map +2 -2
  72. package/build-module/sync.mjs +2 -0
  73. package/build-module/sync.mjs.map +2 -2
  74. package/build-module/types.mjs +9 -0
  75. package/build-module/types.mjs.map +4 -4
  76. package/build-module/utils/block-selection-history.mjs +5 -2
  77. package/build-module/utils/block-selection-history.mjs.map +2 -2
  78. package/build-module/utils/crdt-blocks.mjs +17 -3
  79. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  80. package/build-module/utils/crdt-selection.mjs +8 -2
  81. package/build-module/utils/crdt-selection.mjs.map +2 -2
  82. package/build-module/utils/crdt-user-selections.mjs +10 -7
  83. package/build-module/utils/crdt-user-selections.mjs.map +2 -2
  84. package/build-module/utils/crdt-utils.mjs +51 -1
  85. package/build-module/utils/crdt-utils.mjs.map +2 -2
  86. package/build-module/utils/crdt.mjs +4 -23
  87. package/build-module/utils/crdt.mjs.map +2 -2
  88. package/build-module/utils/index.mjs +2 -0
  89. package/build-module/utils/index.mjs.map +2 -2
  90. package/build-module/utils/normalize-query-for-resolution.mjs +14 -0
  91. package/build-module/utils/normalize-query-for-resolution.mjs.map +7 -0
  92. package/build-types/actions.d.ts.map +1 -1
  93. package/build-types/awareness/post-editor-awareness.d.ts +2 -2
  94. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  95. package/build-types/awareness/types.d.ts +1 -1
  96. package/build-types/awareness/types.d.ts.map +1 -1
  97. package/build-types/entities.d.ts +1 -1
  98. package/build-types/entities.d.ts.map +1 -1
  99. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  100. package/build-types/hooks/use-post-editor-awareness-state.d.ts +34 -10
  101. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  102. package/build-types/index.d.ts +2 -0
  103. package/build-types/index.d.ts.map +1 -1
  104. package/build-types/private-apis.d.ts.map +1 -1
  105. package/build-types/queried-data/get-query-parts.d.ts +7 -0
  106. package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
  107. package/build-types/queried-data/selectors.d.ts.map +1 -1
  108. package/build-types/reducer.d.ts.map +1 -1
  109. package/build-types/resolvers.d.ts +2 -1
  110. package/build-types/resolvers.d.ts.map +1 -1
  111. package/build-types/selectors.d.ts +17 -0
  112. package/build-types/selectors.d.ts.map +1 -1
  113. package/build-types/sync.d.ts +2 -2
  114. package/build-types/sync.d.ts.map +1 -1
  115. package/build-types/types.d.ts +18 -1
  116. package/build-types/types.d.ts.map +1 -1
  117. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  118. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  119. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  120. package/build-types/utils/crdt-user-selections.d.ts +9 -5
  121. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  122. package/build-types/utils/crdt-utils.d.ts +20 -0
  123. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  124. package/build-types/utils/crdt.d.ts +6 -7
  125. package/build-types/utils/crdt.d.ts.map +1 -1
  126. package/build-types/utils/index.d.ts +1 -0
  127. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  128. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  129. package/build-types/utils/test/crdt-utils.d.ts +2 -0
  130. package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
  131. package/package.json +18 -18
  132. package/src/actions.js +25 -40
  133. package/src/awareness/post-editor-awareness.ts +106 -7
  134. package/src/awareness/test/post-editor-awareness.ts +50 -10
  135. package/src/awareness/types.ts +1 -1
  136. package/src/entities.js +38 -6
  137. package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
  138. package/src/hooks/use-entity-prop.js +2 -0
  139. package/src/hooks/use-post-editor-awareness-state.ts +160 -8
  140. package/src/index.js +1 -0
  141. package/src/private-apis.js +6 -2
  142. package/src/queried-data/get-query-parts.js +13 -0
  143. package/src/queried-data/selectors.js +33 -8
  144. package/src/queried-data/test/get-query-parts.js +34 -0
  145. package/src/queried-data/test/selectors.js +183 -0
  146. package/src/reducer.js +11 -0
  147. package/src/resolvers.js +136 -88
  148. package/src/selectors.ts +56 -0
  149. package/src/sync.ts +2 -0
  150. package/src/test/entities.js +185 -1
  151. package/src/test/resolvers.js +64 -11
  152. package/src/test/selectors.js +150 -0
  153. package/src/test/store.js +66 -0
  154. package/src/types.ts +26 -1
  155. package/src/utils/block-selection-history.ts +5 -2
  156. package/src/utils/crdt-blocks.ts +32 -3
  157. package/src/utils/crdt-selection.ts +8 -2
  158. package/src/utils/crdt-user-selections.ts +20 -8
  159. package/src/utils/crdt-utils.ts +99 -0
  160. package/src/utils/crdt.ts +8 -32
  161. package/src/utils/index.js +1 -0
  162. package/src/utils/normalize-query-for-resolution.js +23 -0
  163. package/src/utils/test/crdt-blocks.ts +146 -0
  164. package/src/utils/test/crdt-user-selections.ts +5 -0
  165. package/src/utils/test/crdt-utils.ts +387 -0
  166. package/src/utils/test/crdt.ts +120 -53
@@ -2,6 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { Y } from '@wordpress/sync';
5
+ import { create, insert, toHTMLString } from '@wordpress/rich-text';
5
6
 
6
7
  /**
7
8
  * Internal dependencies
@@ -104,6 +105,104 @@ export function findBlockByClientIdInDoc(
104
105
  return findBlockByClientIdInBlocks( blockId, blocks );
105
106
  }
106
107
 
108
+ // Marker for insertion.
109
+ const MARKER_START = 0xe000;
110
+
111
+ /**
112
+ * Pick a marker character that does not appear in `text`. Returns the marker
113
+ * or `null` if all candidates are present (extremely unlikely in practice).
114
+ *
115
+ * @param text The string to check for existing marker characters.
116
+ */
117
+ function pickMarker( text: string ): string | null {
118
+ const tryCount = 0x10;
119
+
120
+ // Scan the unicode private use area for the first code point not present
121
+ // in the text.
122
+ for ( let code = MARKER_START; code < MARKER_START + tryCount; code++ ) {
123
+ const candidate = String.fromCharCode( code );
124
+
125
+ if ( ! text.includes( candidate ) ) {
126
+ return candidate;
127
+ }
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Convert an HTML character index (counting tag characters) to a rich-text
135
+ * offset (counting only text characters). Used on read paths where Y.Text
136
+ * resolves to an HTML index but the block editor expects a text offset.
137
+ *
138
+ * @param html The full HTML string from Y.Text.
139
+ * @param htmlIndex The HTML character index.
140
+ * @return The corresponding rich-text offset.
141
+ */
142
+ export function htmlIndexToRichTextOffset(
143
+ html: string,
144
+ htmlIndex: number
145
+ ): number {
146
+ if ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {
147
+ return htmlIndex;
148
+ }
149
+
150
+ const marker = pickMarker( html );
151
+ if ( ! marker ) {
152
+ return htmlIndex;
153
+ }
154
+
155
+ // Insert marker and let create() do the parsing.
156
+ const withMarker =
157
+ html.slice( 0, htmlIndex ) + marker + html.slice( htmlIndex );
158
+ const value = create( { html: withMarker } );
159
+ const markerPos = value.text.indexOf( marker );
160
+
161
+ return markerPos === -1 ? htmlIndex : markerPos;
162
+ }
163
+
164
+ /**
165
+ * Convert a rich-text offset (counting only text characters) to an HTML
166
+ * character index (counting tag characters). Used on write paths where the
167
+ * block editor provides a text offset but Y.Text expects an HTML index.
168
+ *
169
+ * @param html The full HTML string from Y.Text.
170
+ * @param richTextOffset The rich-text text offset.
171
+ * @return The corresponding HTML character index.
172
+ */
173
+ export function richTextOffsetToHtmlIndex(
174
+ html: string,
175
+ richTextOffset: number
176
+ ): number {
177
+ if ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {
178
+ return richTextOffset;
179
+ }
180
+
181
+ const marker = pickMarker( html );
182
+ if ( ! marker ) {
183
+ return richTextOffset;
184
+ }
185
+
186
+ const value = create( { html } );
187
+ const markerValue = create( { text: marker } );
188
+ // The marker must inherit the formatting at the insertion point so that
189
+ // toHTMLString does not split surrounding tags (e.g. <strong>) around it.
190
+ if ( value.formats[ richTextOffset ] ) {
191
+ markerValue.formats[ 0 ] = value.formats[ richTextOffset ];
192
+ }
193
+
194
+ const withMarker = insert(
195
+ value,
196
+ markerValue,
197
+ richTextOffset,
198
+ richTextOffset
199
+ );
200
+
201
+ const htmlWithMarker = toHTMLString( { value: withMarker } );
202
+ const markerIndex = htmlWithMarker.indexOf( marker );
203
+ return markerIndex === -1 ? richTextOffset : markerIndex;
204
+ }
205
+
107
206
  function findBlockByClientIdInBlocks(
108
207
  blockId: string,
109
208
  blocks: YBlocks
package/src/utils/crdt.ts CHANGED
@@ -27,7 +27,6 @@ import {
27
27
  type YBlocks,
28
28
  } from './crdt-blocks';
29
29
  import { type Post } from '../entity-types/post';
30
- import { type Type } from '../entity-types';
31
30
  import { CRDT_DOC_META_PERSISTENCE_KEY, CRDT_RECORD_MAP_KEY } from '../sync';
32
31
  import type { WPSelection } from '../types';
33
32
  import {
@@ -76,27 +75,6 @@ export interface YPostRecord extends YMapRecord {
76
75
 
77
76
  export const POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE = '_crdt_document';
78
77
 
79
- // Properties that are allowed to be synced for a post.
80
- const allowedPostProperties = new Set< string >( [
81
- 'author',
82
- 'blocks',
83
- 'content',
84
- 'categories',
85
- 'comment_status',
86
- 'date',
87
- 'excerpt',
88
- 'featured_media',
89
- 'format',
90
- 'meta',
91
- 'ping_status',
92
- 'slug',
93
- 'status',
94
- 'sticky',
95
- 'tags',
96
- 'template',
97
- 'title',
98
- ] );
99
-
100
78
  // Post meta keys that should *not* be synced.
101
79
  const disallowedPostMetaKeys = new Set< string >( [
102
80
  POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
@@ -139,18 +117,18 @@ function defaultApplyChangesToCRDTDoc(
139
117
  *
140
118
  * @param {CRDTDoc} ydoc
141
119
  * @param {PostChanges} changes
142
- * @param {Type} _postType
120
+ * @param {Set<string>} syncedProperties
143
121
  * @return {void}
144
122
  */
145
123
  export function applyPostChangesToCRDTDoc(
146
124
  ydoc: CRDTDoc,
147
125
  changes: PostChanges,
148
- _postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
126
+ syncedProperties: Set< string >
149
127
  ): void {
150
128
  const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
151
129
 
152
130
  Object.keys( changes ).forEach( ( key ) => {
153
- if ( ! allowedPostProperties.has( key ) ) {
131
+ if ( ! syncedProperties.has( key ) ) {
154
132
  return;
155
133
  }
156
134
 
@@ -289,15 +267,15 @@ function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
289
267
  * against the local record and determine if there are changes (edits) we want
290
268
  * to dispatch.
291
269
  *
292
- * @param {CRDTDoc} ydoc
293
- * @param {Post} editedRecord
294
- * @param {Type} _postType
270
+ * @param {CRDTDoc} ydoc
271
+ * @param {Post} editedRecord
272
+ * @param {Set<string>} syncedProperties
295
273
  * @return {Partial<PostChanges>} The changes that should be applied to the local record.
296
274
  */
297
275
  export function getPostChangesFromCRDTDoc(
298
276
  ydoc: CRDTDoc,
299
277
  editedRecord: Post,
300
- _postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
278
+ syncedProperties: Set< string >
301
279
  ): PostChanges {
302
280
  const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
303
281
 
@@ -305,7 +283,7 @@ export function getPostChangesFromCRDTDoc(
305
283
 
306
284
  const changes = Object.fromEntries(
307
285
  Object.entries( ymap.toJSON() ).filter( ( [ key, newValue ] ) => {
308
- if ( ! allowedPostProperties.has( key ) ) {
286
+ if ( ! syncedProperties.has( key ) ) {
309
287
  return false;
310
288
  }
311
289
 
@@ -342,8 +320,6 @@ export function getPostChangesFromCRDTDoc(
342
320
  );
343
321
  }
344
322
 
345
- // The consumers of blocks have memoization that renders optimization
346
- // here unnecessary.
347
323
  return true;
348
324
  }
349
325
 
@@ -15,3 +15,4 @@ export {
15
15
  ALLOWED_RESOURCE_ACTIONS,
16
16
  } from './user-permissions';
17
17
  export { RECEIVE_INTERMEDIATE_RESULTS } from './receive-intermediate-results';
18
+ export { default as normalizeQueryForResolution } from './normalize-query-for-resolution';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Returns a copy of `query` filtered to only the keys that affect resolution
3
+ * identity (`context` and `_fields`), or `undefined` when none are present.
4
+ * This mirrors the normalisation that the data store applies when keying
5
+ * resolved records, so that `finishResolutions` receives args that match the
6
+ * keys used by callers who omit pagination params.
7
+ *
8
+ * @param {Object} query The raw query object.
9
+ * @return {Object|undefined} Normalised query or undefined.
10
+ */
11
+ export default function normalizeQueryForResolution( query ) {
12
+ if ( ! query ) {
13
+ return undefined;
14
+ }
15
+
16
+ const entries = Object.entries( query ).filter(
17
+ ( [ k, v ] ) =>
18
+ ( k === 'context' || k === '_fields' ) &&
19
+ v !== undefined &&
20
+ v !== null
21
+ );
22
+ return entries.length > 0 ? Object.fromEntries( entries ) : undefined;
23
+ }
@@ -38,9 +38,24 @@ jest.mock( '@wordpress/blocks', () => ( {
38
38
  url: { type: 'string' },
39
39
  },
40
40
  },
41
+ {
42
+ name: 'core/table',
43
+ attributes: {
44
+ hasFixedLayout: { type: 'boolean' },
45
+ caption: { type: 'rich-text' },
46
+ head: { type: 'array' },
47
+ body: { type: 'array' },
48
+ foot: { type: 'array' },
49
+ },
50
+ },
41
51
  ],
42
52
  } ) );
43
53
 
54
+ /**
55
+ * WordPress dependencies
56
+ */
57
+ import { RichTextData } from '@wordpress/rich-text';
58
+
44
59
  /**
45
60
  * Internal dependencies
46
61
  */
@@ -1089,6 +1104,137 @@ describe( 'crdt-blocks', () => {
1089
1104
  } );
1090
1105
  } );
1091
1106
 
1107
+ describe( 'table block', () => {
1108
+ it( 'preserves table cell content through CRDT round-trip', () => {
1109
+ const tableBlocks: Block[] = [
1110
+ {
1111
+ name: 'core/table',
1112
+ attributes: {
1113
+ hasFixedLayout: true,
1114
+ body: [
1115
+ {
1116
+ cells: [
1117
+ {
1118
+ content:
1119
+ RichTextData.fromPlainText( '1' ),
1120
+ tag: 'td',
1121
+ },
1122
+ {
1123
+ content:
1124
+ RichTextData.fromPlainText( '2' ),
1125
+ tag: 'td',
1126
+ },
1127
+ ],
1128
+ },
1129
+ {
1130
+ cells: [
1131
+ {
1132
+ content:
1133
+ RichTextData.fromPlainText( '3' ),
1134
+ tag: 'td',
1135
+ },
1136
+ {
1137
+ content:
1138
+ RichTextData.fromPlainText( '4' ),
1139
+ tag: 'td',
1140
+ },
1141
+ ],
1142
+ },
1143
+ ],
1144
+ },
1145
+ innerBlocks: [],
1146
+ },
1147
+ ];
1148
+
1149
+ mergeCrdtBlocks( yblocks, tableBlocks, null );
1150
+
1151
+ // Simulate a CRDT encode/decode cycle (persistence or sync).
1152
+ const encoded = Y.encodeStateAsUpdate( doc );
1153
+ const doc2 = new Y.Doc();
1154
+ Y.applyUpdate( doc2, encoded );
1155
+
1156
+ const yblocks2 = doc2.getArray< YBlock >();
1157
+ expect( yblocks2.length ).toBe( 1 );
1158
+
1159
+ const block = yblocks2.get( 0 );
1160
+ const attrs = block.get( 'attributes' ) as YBlockAttributes;
1161
+ const body = attrs.get( 'body' ) as {
1162
+ cells: { content: string; tag: string }[];
1163
+ }[];
1164
+
1165
+ expect( body ).toHaveLength( 2 );
1166
+ expect( body[ 0 ].cells[ 0 ].content ).toBe( '1' );
1167
+ expect( body[ 0 ].cells[ 1 ].content ).toBe( '2' );
1168
+ expect( body[ 1 ].cells[ 0 ].content ).toBe( '3' );
1169
+ expect( body[ 1 ].cells[ 1 ].content ).toBe( '4' );
1170
+
1171
+ doc2.destroy();
1172
+ } );
1173
+
1174
+ it( 'preserves table cell content with HTML formatting', () => {
1175
+ const tableBlocks: Block[] = [
1176
+ {
1177
+ name: 'core/table',
1178
+ attributes: {
1179
+ hasFixedLayout: true,
1180
+ head: [
1181
+ {
1182
+ cells: [
1183
+ {
1184
+ content: RichTextData.fromHTMLString(
1185
+ '<strong>Header</strong>'
1186
+ ),
1187
+ tag: 'th',
1188
+ },
1189
+ ],
1190
+ },
1191
+ ],
1192
+ body: [
1193
+ {
1194
+ cells: [
1195
+ {
1196
+ content: RichTextData.fromHTMLString(
1197
+ '<a href="https://example.com">Link</a>'
1198
+ ),
1199
+ tag: 'td',
1200
+ },
1201
+ ],
1202
+ },
1203
+ ],
1204
+ },
1205
+ innerBlocks: [],
1206
+ },
1207
+ ];
1208
+
1209
+ mergeCrdtBlocks( yblocks, tableBlocks, null );
1210
+
1211
+ // Round-trip through encode/decode.
1212
+ const encoded = Y.encodeStateAsUpdate( doc );
1213
+ const doc2 = new Y.Doc();
1214
+ Y.applyUpdate( doc2, encoded );
1215
+
1216
+ const yblocks2 = doc2.getArray< YBlock >();
1217
+ const block = yblocks2.get( 0 );
1218
+ const attrs = block.get( 'attributes' ) as YBlockAttributes;
1219
+
1220
+ const head = attrs.get( 'head' ) as {
1221
+ cells: { content: string }[];
1222
+ }[];
1223
+ expect( head[ 0 ].cells[ 0 ].content ).toBe(
1224
+ '<strong>Header</strong>'
1225
+ );
1226
+
1227
+ const body = attrs.get( 'body' ) as {
1228
+ cells: { content: string }[];
1229
+ }[];
1230
+ expect( body[ 0 ].cells[ 0 ].content ).toBe(
1231
+ '<a href="https://example.com">Link</a>'
1232
+ );
1233
+
1234
+ doc2.destroy();
1235
+ } );
1236
+ } );
1237
+
1092
1238
  describe( 'emoji handling', () => {
1093
1239
  // Emoji like 😀 (U+1F600) are surrogate pairs in UTF-16 (.length === 2).
1094
1240
  // The CRDT sync must preserve them without corruption (no U+FFFD / '�').
@@ -16,6 +16,11 @@ import { CRDT_RECORD_MAP_KEY } from '../../sync';
16
16
 
17
17
  jest.mock( '@wordpress/data', () => ( {
18
18
  select: jest.fn(),
19
+ // Needed because @wordpress/rich-text initialises its store at import time.
20
+ combineReducers: jest.fn( () => jest.fn( () => ( {} ) ) ),
21
+ createReduxStore: jest.fn( () => ( {} ) ),
22
+ register: jest.fn(),
23
+ createSelector: ( selector: Function ) => selector,
19
24
  } ) );
20
25
 
21
26
  jest.mock( '@wordpress/block-editor', () => ( {