@wordpress/core-data 7.40.2-next.v.202602241322.0 → 7.40.2

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 (183) hide show
  1. package/README.md +0 -27
  2. package/build/actions.cjs +9 -26
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/post-editor-awareness.cjs +45 -6
  5. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  6. package/build/awareness/types.cjs.map +1 -1
  7. package/build/entities.cjs +50 -23
  8. package/build/entities.cjs.map +2 -2
  9. package/build/entity-provider.cjs +15 -6
  10. package/build/entity-provider.cjs.map +2 -2
  11. package/build/hooks/use-entity-prop.cjs +33 -2
  12. package/build/hooks/use-entity-prop.cjs.map +2 -2
  13. package/build/hooks/use-post-editor-awareness-state.cjs +120 -1
  14. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  15. package/build/index.cjs +0 -3
  16. package/build/index.cjs.map +2 -2
  17. package/build/private-actions.cjs +27 -3
  18. package/build/private-actions.cjs.map +2 -2
  19. package/build/private-apis.cjs +14 -5
  20. package/build/private-apis.cjs.map +3 -3
  21. package/build/private-selectors.cjs +22 -2
  22. package/build/private-selectors.cjs.map +2 -2
  23. package/build/queried-data/actions.cjs +1 -1
  24. package/build/queried-data/actions.cjs.map +2 -2
  25. package/build/queried-data/reducer.cjs +19 -13
  26. package/build/queried-data/reducer.cjs.map +2 -2
  27. package/build/reducer.cjs +19 -2
  28. package/build/reducer.cjs.map +2 -2
  29. package/build/resolvers.cjs +16 -6
  30. package/build/resolvers.cjs.map +2 -2
  31. package/build/selectors.cjs +0 -15
  32. package/build/selectors.cjs.map +2 -2
  33. package/build/sync.cjs +11 -5
  34. package/build/sync.cjs.map +2 -2
  35. package/build/types.cjs.map +1 -1
  36. package/build/utils/block-selection-history.cjs +1 -1
  37. package/build/utils/block-selection-history.cjs.map +2 -2
  38. package/build/utils/crdt-blocks.cjs +125 -49
  39. package/build/utils/crdt-blocks.cjs.map +2 -2
  40. package/build/utils/crdt-selection.cjs +50 -19
  41. package/build/utils/crdt-selection.cjs.map +2 -2
  42. package/build/utils/crdt-text.cjs +52 -0
  43. package/build/utils/crdt-text.cjs.map +7 -0
  44. package/build/utils/crdt-user-selections.cjs +16 -6
  45. package/build/utils/crdt-user-selections.cjs.map +3 -3
  46. package/build/utils/crdt-utils.cjs +54 -2
  47. package/build/utils/crdt-utils.cjs.map +2 -2
  48. package/build/utils/crdt.cjs +38 -29
  49. package/build/utils/crdt.cjs.map +2 -2
  50. package/build-module/actions.mjs +14 -26
  51. package/build-module/actions.mjs.map +2 -2
  52. package/build-module/awareness/post-editor-awareness.mjs +47 -7
  53. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  54. package/build-module/entities.mjs +53 -24
  55. package/build-module/entities.mjs.map +2 -2
  56. package/build-module/entity-provider.mjs +15 -6
  57. package/build-module/entity-provider.mjs.map +2 -2
  58. package/build-module/hooks/use-entity-prop.mjs +34 -3
  59. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  60. package/build-module/hooks/use-post-editor-awareness-state.mjs +117 -1
  61. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  62. package/build-module/index.mjs +0 -2
  63. package/build-module/index.mjs.map +2 -2
  64. package/build-module/private-actions.mjs +24 -2
  65. package/build-module/private-actions.mjs.map +2 -2
  66. package/build-module/private-apis.mjs +21 -6
  67. package/build-module/private-apis.mjs.map +3 -3
  68. package/build-module/private-selectors.mjs +19 -1
  69. package/build-module/private-selectors.mjs.map +2 -2
  70. package/build-module/queried-data/actions.mjs +1 -1
  71. package/build-module/queried-data/actions.mjs.map +2 -2
  72. package/build-module/queried-data/reducer.mjs +19 -13
  73. package/build-module/queried-data/reducer.mjs.map +2 -2
  74. package/build-module/reducer.mjs +18 -2
  75. package/build-module/reducer.mjs.map +2 -2
  76. package/build-module/resolvers.mjs +16 -6
  77. package/build-module/resolvers.mjs.map +2 -2
  78. package/build-module/selectors.mjs +0 -14
  79. package/build-module/selectors.mjs.map +2 -2
  80. package/build-module/sync.mjs +7 -3
  81. package/build-module/sync.mjs.map +2 -2
  82. package/build-module/utils/block-selection-history.mjs +5 -2
  83. package/build-module/utils/block-selection-history.mjs.map +2 -2
  84. package/build-module/utils/crdt-blocks.mjs +124 -49
  85. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  86. package/build-module/utils/crdt-selection.mjs +53 -20
  87. package/build-module/utils/crdt-selection.mjs.map +2 -2
  88. package/build-module/utils/crdt-text.mjs +26 -0
  89. package/build-module/utils/crdt-text.mjs.map +7 -0
  90. package/build-module/utils/crdt-user-selections.mjs +16 -7
  91. package/build-module/utils/crdt-user-selections.mjs.map +3 -3
  92. package/build-module/utils/crdt-utils.mjs +51 -1
  93. package/build-module/utils/crdt-utils.mjs.map +2 -2
  94. package/build-module/utils/crdt.mjs +42 -34
  95. package/build-module/utils/crdt.mjs.map +2 -2
  96. package/build-types/actions.d.ts +0 -11
  97. package/build-types/actions.d.ts.map +1 -1
  98. package/build-types/awareness/post-editor-awareness.d.ts +2 -2
  99. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  100. package/build-types/awareness/types.d.ts +6 -1
  101. package/build-types/awareness/types.d.ts.map +1 -1
  102. package/build-types/entities.d.ts +20 -1
  103. package/build-types/entities.d.ts.map +1 -1
  104. package/build-types/entity-provider.d.ts +11 -6
  105. package/build-types/entity-provider.d.ts.map +1 -1
  106. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  107. package/build-types/hooks/use-post-editor-awareness-state.d.ts +40 -7
  108. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  109. package/build-types/index.d.ts +0 -3
  110. package/build-types/index.d.ts.map +1 -1
  111. package/build-types/parsed-blocks-cache.d.ts +10 -0
  112. package/build-types/parsed-blocks-cache.d.ts.map +1 -0
  113. package/build-types/private-actions.d.ts +12 -0
  114. package/build-types/private-actions.d.ts.map +1 -1
  115. package/build-types/private-apis.d.ts +21 -1
  116. package/build-types/private-apis.d.ts.map +1 -1
  117. package/build-types/private-selectors.d.ts +17 -0
  118. package/build-types/private-selectors.d.ts.map +1 -1
  119. package/build-types/queried-data/reducer.d.ts.map +1 -1
  120. package/build-types/reducer.d.ts +15 -0
  121. package/build-types/reducer.d.ts.map +1 -1
  122. package/build-types/resolvers.d.ts.map +1 -1
  123. package/build-types/selectors.d.ts +1 -9
  124. package/build-types/selectors.d.ts.map +1 -1
  125. package/build-types/sync.d.ts +2 -2
  126. package/build-types/sync.d.ts.map +1 -1
  127. package/build-types/types.d.ts +11 -2
  128. package/build-types/types.d.ts.map +1 -1
  129. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  130. package/build-types/utils/crdt-blocks.d.ts +12 -1
  131. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  132. package/build-types/utils/crdt-selection.d.ts +10 -0
  133. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  134. package/build-types/utils/crdt-text.d.ts +16 -0
  135. package/build-types/utils/crdt-text.d.ts.map +1 -0
  136. package/build-types/utils/crdt-user-selections.d.ts +17 -4
  137. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  138. package/build-types/utils/crdt-utils.d.ts +20 -0
  139. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  140. package/build-types/utils/crdt.d.ts +14 -7
  141. package/build-types/utils/crdt.d.ts.map +1 -1
  142. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  143. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  144. package/build-types/utils/save-crdt-doc.d.ts +8 -0
  145. package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
  146. package/build-types/utils/test/crdt-utils.d.ts +2 -0
  147. package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
  148. package/package.json +18 -18
  149. package/src/actions.js +26 -41
  150. package/src/awareness/post-editor-awareness.ts +106 -7
  151. package/src/awareness/test/post-editor-awareness.ts +50 -10
  152. package/src/awareness/types.ts +7 -1
  153. package/src/entities.js +62 -24
  154. package/src/entity-provider.js +24 -11
  155. package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
  156. package/src/hooks/use-entity-prop.js +43 -3
  157. package/src/hooks/use-post-editor-awareness-state.ts +229 -7
  158. package/src/index.js +0 -6
  159. package/src/private-actions.js +43 -1
  160. package/src/{private-apis.js → private-apis.ts} +21 -3
  161. package/src/private-selectors.ts +43 -0
  162. package/src/queried-data/actions.js +1 -1
  163. package/src/queried-data/reducer.js +26 -14
  164. package/src/reducer.js +36 -1
  165. package/src/resolvers.js +23 -7
  166. package/src/selectors.ts +1 -32
  167. package/src/sync.ts +6 -2
  168. package/src/test/entities.js +231 -14
  169. package/src/test/private-actions.js +1 -1
  170. package/src/test/resolvers.js +64 -11
  171. package/src/test/store.js +116 -0
  172. package/src/types.ts +21 -2
  173. package/src/utils/block-selection-history.ts +5 -2
  174. package/src/utils/crdt-blocks.ts +250 -81
  175. package/src/utils/crdt-selection.ts +92 -26
  176. package/src/utils/crdt-text.ts +43 -0
  177. package/src/utils/crdt-user-selections.ts +29 -8
  178. package/src/utils/crdt-utils.ts +99 -0
  179. package/src/utils/crdt.ts +72 -46
  180. package/src/utils/test/crdt-blocks.ts +1177 -22
  181. package/src/utils/test/crdt-user-selections.ts +5 -0
  182. package/src/utils/test/crdt-utils.ts +387 -0
  183. package/src/utils/test/crdt.ts +423 -60
@@ -8,22 +8,117 @@ import { Y } from '@wordpress/sync';
8
8
  */
9
9
  import { describe, expect, it, jest, beforeEach } from '@jest/globals';
10
10
 
11
+ /**
12
+ * Mock getBlockTypes so isRichTextAttribute can identify rich-text attrs.
13
+ */
14
+ jest.mock( '@wordpress/blocks', () => {
15
+ const actual = jest.requireActual( '@wordpress/blocks' ) as Record<
16
+ string,
17
+ unknown
18
+ >;
19
+ return {
20
+ ...actual,
21
+ getBlockTypes: () => [
22
+ {
23
+ name: 'core/paragraph',
24
+ attributes: { content: { type: 'rich-text' } },
25
+ },
26
+ {
27
+ name: 'core/table',
28
+ attributes: {
29
+ hasFixedLayout: { type: 'boolean' },
30
+ caption: { type: 'rich-text' },
31
+ body: {
32
+ type: 'array',
33
+ query: {
34
+ cells: {
35
+ type: 'array',
36
+ query: {
37
+ content: { type: 'rich-text' },
38
+ tag: { type: 'string' },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ ],
46
+ };
47
+ } );
48
+
49
+ /**
50
+ * WordPress dependencies
51
+ */
52
+ import { RichTextData } from '@wordpress/rich-text';
53
+
11
54
  /**
12
55
  * Internal dependencies
13
56
  */
14
- import {
15
- CRDT_RECORD_MAP_KEY,
16
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
17
- } from '../../sync';
57
+ import { CRDT_RECORD_MAP_KEY } from '../../sync';
18
58
  import {
19
59
  applyPostChangesToCRDTDoc,
60
+ defaultCollectionSyncConfig,
20
61
  getPostChangesFromCRDTDoc,
62
+ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
21
63
  type PostChanges,
22
64
  type YPostRecord,
23
65
  } from '../crdt';
24
- import type { YBlock, YBlocks } from '../crdt-blocks';
66
+ import type { YBlock, YBlockRecord, YBlocks } from '../crdt-blocks';
67
+ import { updateSelectionHistory } from '../crdt-selection';
25
68
  import { createYMap, getRootMap, type YMapWrap } from '../crdt-utils';
26
- import type { Post, Type } from '../../entity-types';
69
+ import type { Post } from '../../entity-types';
70
+
71
+ // Default synced properties matching the base set built in entities.js,
72
+ // plus 'categories' and 'tags' as example taxonomy rest_base values.
73
+ const defaultSyncedProperties = new Set< string >( [
74
+ 'blocks',
75
+ 'categories',
76
+ 'content',
77
+ 'date',
78
+ 'excerpt',
79
+ 'meta',
80
+ 'slug',
81
+ 'status',
82
+ 'tags',
83
+ 'title',
84
+ ] );
85
+
86
+ describe( 'defaultCollectionSyncConfig', () => {
87
+ it( 'has no-op applyChangesToCRDTDoc', () => {
88
+ const doc = new Y.Doc();
89
+ // Should not throw and return undefined.
90
+ expect(
91
+ defaultCollectionSyncConfig.applyChangesToCRDTDoc( doc, {
92
+ title: 'test',
93
+ } )
94
+ ).toBeUndefined();
95
+ doc.destroy();
96
+ } );
97
+
98
+ it( 'has getChangesFromCRDTDoc that returns empty object', () => {
99
+ const doc = new Y.Doc();
100
+ const result = defaultCollectionSyncConfig.getChangesFromCRDTDoc( doc, {
101
+ title: 'test',
102
+ } );
103
+ expect( result ).toEqual( {} );
104
+ doc.destroy();
105
+ } );
106
+
107
+ it( 'shouldSync returns true when objectId is null (collection)', () => {
108
+ expect(
109
+ defaultCollectionSyncConfig.shouldSync?.( 'comment', null )
110
+ ).toBe( true );
111
+ } );
112
+
113
+ it( 'shouldSync returns false when objectId is provided (individual record)', () => {
114
+ expect(
115
+ defaultCollectionSyncConfig.shouldSync?.( 'comment', '123' )
116
+ ).toBe( false );
117
+ expect(
118
+ defaultCollectionSyncConfig.shouldSync?.( 'comment', 'foo' )
119
+ ).toBe( false );
120
+ } );
121
+ } );
27
122
 
28
123
  describe( 'crdt', () => {
29
124
  let doc: Y.Doc;
@@ -38,8 +133,6 @@ describe( 'crdt', () => {
38
133
  } );
39
134
 
40
135
  describe( 'applyPostChangesToCRDTDoc', () => {
41
- const mockPostType = {} as Type;
42
-
43
136
  let map: YMapWrap< YPostRecord >;
44
137
 
45
138
  beforeEach( () => {
@@ -51,7 +144,7 @@ describe( 'crdt', () => {
51
144
  title: 'New Title',
52
145
  } as PostChanges;
53
146
 
54
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
147
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
55
148
 
56
149
  const title = map.get( 'title' );
57
150
  expect( title ).toBeInstanceOf( Y.Text );
@@ -64,7 +157,7 @@ describe( 'crdt', () => {
64
157
  unsyncedProperty: 'value',
65
158
  } as unknown as PostChanges;
66
159
 
67
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
160
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
68
161
 
69
162
  expect( map.has( 'unsyncedProperty' ) ).toBe( false );
70
163
  expect( map.get( 'title' )?.toString() ).toBe( 'New Title' );
@@ -75,7 +168,7 @@ describe( 'crdt', () => {
75
168
  title: () => 'function value',
76
169
  } as unknown as PostChanges;
77
170
 
78
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
171
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
79
172
 
80
173
  expect( map.has( 'title' ) ).toBe( false );
81
174
  } );
@@ -85,7 +178,7 @@ describe( 'crdt', () => {
85
178
  title: { raw: 'Raw Title', rendered: 'Rendered Title' },
86
179
  };
87
180
 
88
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
181
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
89
182
 
90
183
  const title = map.get( 'title' );
91
184
  expect( title ).toBeInstanceOf( Y.Text );
@@ -97,7 +190,7 @@ describe( 'crdt', () => {
97
190
  title: 'Auto Draft',
98
191
  } as PostChanges;
99
192
 
100
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
193
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
101
194
 
102
195
  const title = map.get( 'title' );
103
196
  expect( title ).toBeInstanceOf( Y.Text );
@@ -109,7 +202,7 @@ describe( 'crdt', () => {
109
202
  applyPostChangesToCRDTDoc(
110
203
  doc,
111
204
  { title: '' } as PostChanges,
112
- mockPostType
205
+ defaultSyncedProperties
113
206
  );
114
207
 
115
208
  const title = map.get( 'title' );
@@ -120,7 +213,7 @@ describe( 'crdt', () => {
120
213
  applyPostChangesToCRDTDoc(
121
214
  doc,
122
215
  { title: 'Auto Draft' } as PostChanges,
123
- mockPostType
216
+ defaultSyncedProperties
124
217
  );
125
218
 
126
219
  expect( map.get( 'title' )?.toString() ).toBe( '' );
@@ -135,7 +228,7 @@ describe( 'crdt', () => {
135
228
  },
136
229
  };
137
230
 
138
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
231
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
139
232
 
140
233
  const excerpt = map.get( 'excerpt' );
141
234
  expect( excerpt ).toBeInstanceOf( Y.Text );
@@ -147,7 +240,7 @@ describe( 'crdt', () => {
147
240
  slug: '',
148
241
  };
149
242
 
150
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
243
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
151
244
 
152
245
  expect( map.has( 'slug' ) ).toBe( false );
153
246
  } );
@@ -157,7 +250,7 @@ describe( 'crdt', () => {
157
250
  slug: 'my-post-slug',
158
251
  };
159
252
 
160
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
253
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
161
254
 
162
255
  expect( map.get( 'slug' ) ).toBe( 'my-post-slug' );
163
256
  } );
@@ -175,7 +268,7 @@ describe( 'crdt', () => {
175
268
  ],
176
269
  };
177
270
 
178
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
271
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
179
272
 
180
273
  expect( ( map.get( 'blocks' ) as YBlocks ).toJSON() ).toEqual(
181
274
  changes.blocks
@@ -187,7 +280,7 @@ describe( 'crdt', () => {
187
280
  blocks: [],
188
281
  };
189
282
 
190
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
283
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
191
284
 
192
285
  const blocks = map.get( 'blocks' );
193
286
  expect( blocks ).toBeInstanceOf( Y.Array );
@@ -201,7 +294,7 @@ describe( 'crdt', () => {
201
294
  blocks: undefined,
202
295
  };
203
296
 
204
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
297
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
205
298
 
206
299
  // The key should still exist, but the value should be undefined.
207
300
  expect( map.has( 'blocks' ) ).toBe( true );
@@ -213,7 +306,7 @@ describe( 'crdt', () => {
213
306
  content: 'Hello, world!',
214
307
  } as PostChanges;
215
308
 
216
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
309
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
217
310
 
218
311
  const content = map.get( 'content' );
219
312
  expect( content ).toBeInstanceOf( Y.Text );
@@ -228,7 +321,7 @@ describe( 'crdt', () => {
228
321
  },
229
322
  } as PostChanges;
230
323
 
231
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
324
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
232
325
 
233
326
  const content = map.get( 'content' );
234
327
  expect( content ).toBeInstanceOf( Y.Text );
@@ -242,7 +335,7 @@ describe( 'crdt', () => {
242
335
  applyPostChangesToCRDTDoc(
243
336
  doc,
244
337
  { title: 'Old Title' } as PostChanges,
245
- mockPostType
338
+ defaultSyncedProperties
246
339
  );
247
340
  const titleRef = map.get( 'title' );
248
341
 
@@ -250,7 +343,7 @@ describe( 'crdt', () => {
250
343
  applyPostChangesToCRDTDoc(
251
344
  doc,
252
345
  { title: 'New Title' } as PostChanges,
253
- mockPostType
346
+ defaultSyncedProperties
254
347
  );
255
348
 
256
349
  expect( map.get( 'title' ) ).toBe( titleRef );
@@ -262,7 +355,7 @@ describe( 'crdt', () => {
262
355
  applyPostChangesToCRDTDoc(
263
356
  doc,
264
357
  { content: 'Old content' } as PostChanges,
265
- mockPostType
358
+ defaultSyncedProperties
266
359
  );
267
360
  const contentRef = map.get( 'content' );
268
361
 
@@ -270,7 +363,7 @@ describe( 'crdt', () => {
270
363
  applyPostChangesToCRDTDoc(
271
364
  doc,
272
365
  { content: 'New content' } as PostChanges,
273
- mockPostType
366
+ defaultSyncedProperties
274
367
  );
275
368
 
276
369
  expect( map.get( 'content' ) ).toBe( contentRef );
@@ -282,7 +375,7 @@ describe( 'crdt', () => {
282
375
  applyPostChangesToCRDTDoc(
283
376
  doc,
284
377
  { excerpt: 'Old excerpt' } as PostChanges,
285
- mockPostType
378
+ defaultSyncedProperties
286
379
  );
287
380
  const excerptRef = map.get( 'excerpt' );
288
381
 
@@ -290,7 +383,7 @@ describe( 'crdt', () => {
290
383
  applyPostChangesToCRDTDoc(
291
384
  doc,
292
385
  { excerpt: 'New excerpt' } as PostChanges,
293
- mockPostType
386
+ defaultSyncedProperties
294
387
  );
295
388
 
296
389
  expect( map.get( 'excerpt' ) ).toBe( excerptRef );
@@ -308,7 +401,7 @@ describe( 'crdt', () => {
308
401
  metaMap.set( 'some_meta', 'old value' );
309
402
  map.set( 'meta', metaMap );
310
403
 
311
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
404
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
312
405
 
313
406
  expect( metaMap.get( 'some_meta' ) ).toBe( 'new value' );
314
407
  } );
@@ -324,7 +417,7 @@ describe( 'crdt', () => {
324
417
  metaMap.set( 'some_meta', 'old value' );
325
418
  map.set( 'meta', metaMap );
326
419
 
327
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
420
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
328
421
 
329
422
  expect( metaMap.get( 'some_meta' ) ).toStrictEqual( [
330
423
  'value',
@@ -339,23 +432,40 @@ describe( 'crdt', () => {
339
432
  },
340
433
  };
341
434
 
342
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
435
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
343
436
 
344
437
  const metaMap = map.get( 'meta' );
345
438
  expect( metaMap ).toBeInstanceOf( Y.Map );
346
439
  expect( metaMap?.get( 'custom_field' ) ).toBe( 'value' );
347
440
  } );
441
+
442
+ it( 'syncs taxonomy rest_base values included in syncedProperties', () => {
443
+ const changes = {
444
+ categories: [ 1, 2, 3 ],
445
+ genre: [ 10, 20 ], // should be ignored
446
+ tags: [ 4, 5 ],
447
+ } as unknown as PostChanges;
448
+
449
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
450
+
451
+ expect( map.get( 'categories' ) ).toEqual( [ 1, 2, 3 ] );
452
+ expect( map.get( 'genre' ) ).toBeUndefined();
453
+ expect( map.get( 'tags' ) ).toEqual( [ 4, 5 ] );
454
+
455
+ const customSyncedProperties = new Set( [
456
+ ...defaultSyncedProperties,
457
+ 'genre', // now included
458
+ ] );
459
+
460
+ applyPostChangesToCRDTDoc( doc, changes, customSyncedProperties );
461
+
462
+ expect( map.get( 'categories' ) ).toEqual( [ 1, 2, 3 ] );
463
+ expect( map.get( 'genre' ) ).toEqual( [ 10, 20 ] );
464
+ expect( map.get( 'tags' ) ).toEqual( [ 4, 5 ] );
465
+ } );
348
466
  } );
349
467
 
350
468
  describe( 'getPostChangesFromCRDTDoc', () => {
351
- const mockPostType = {
352
- slug: 'post',
353
- supports: {
354
- title: true,
355
- editor: true,
356
- },
357
- } as unknown as Type;
358
-
359
469
  let map: YMapWrap< YPostRecord >;
360
470
 
361
471
  beforeEach( () => {
@@ -374,7 +484,7 @@ describe( 'crdt', () => {
374
484
  const changes = getPostChangesFromCRDTDoc(
375
485
  doc,
376
486
  editedRecord,
377
- mockPostType
487
+ defaultSyncedProperties
378
488
  );
379
489
 
380
490
  expect( changes.title ).toBe( 'CRDT Title' );
@@ -389,7 +499,7 @@ describe( 'crdt', () => {
389
499
  const changes = getPostChangesFromCRDTDoc(
390
500
  doc,
391
501
  editedRecord,
392
- mockPostType
502
+ defaultSyncedProperties
393
503
  );
394
504
 
395
505
  expect( changes ).not.toHaveProperty( 'unsyncedProp' );
@@ -406,7 +516,7 @@ describe( 'crdt', () => {
406
516
  const changes = getPostChangesFromCRDTDoc(
407
517
  doc,
408
518
  editedRecord,
409
- mockPostType
519
+ defaultSyncedProperties
410
520
  );
411
521
 
412
522
  expect( changes ).not.toHaveProperty( 'status' );
@@ -425,7 +535,7 @@ describe( 'crdt', () => {
425
535
  const changesWithEmptyDate = getPostChangesFromCRDTDoc(
426
536
  doc,
427
537
  editedRecord,
428
- mockPostType
538
+ defaultSyncedProperties
429
539
  );
430
540
 
431
541
  expect( changesWithEmptyDate ).not.toHaveProperty( 'date' );
@@ -435,7 +545,7 @@ describe( 'crdt', () => {
435
545
  const changesWithDefinedDate = getPostChangesFromCRDTDoc(
436
546
  doc,
437
547
  editedRecord,
438
- mockPostType
548
+ defaultSyncedProperties
439
549
  );
440
550
 
441
551
  expect( changesWithDefinedDate ).not.toHaveProperty( 'date' );
@@ -454,7 +564,7 @@ describe( 'crdt', () => {
454
564
  const changesWithEmptyDate = getPostChangesFromCRDTDoc(
455
565
  doc,
456
566
  editedRecord,
457
- mockPostType
567
+ defaultSyncedProperties
458
568
  );
459
569
 
460
570
  expect( changesWithEmptyDate ).not.toHaveProperty( 'date' );
@@ -464,7 +574,7 @@ describe( 'crdt', () => {
464
574
  const changesWithDefinedDate = getPostChangesFromCRDTDoc(
465
575
  doc,
466
576
  editedRecord,
467
- mockPostType
577
+ defaultSyncedProperties
468
578
  );
469
579
 
470
580
  expect( changesWithDefinedDate ).not.toHaveProperty( 'date' );
@@ -480,12 +590,74 @@ describe( 'crdt', () => {
480
590
  const changes = getPostChangesFromCRDTDoc(
481
591
  doc,
482
592
  editedRecord,
483
- mockPostType
593
+ defaultSyncedProperties
484
594
  );
485
595
 
486
596
  expect( changes ).toHaveProperty( 'blocks' );
487
597
  } );
488
598
 
599
+ it( 'returns rich-text block attributes as RichTextData, not strings', () => {
600
+ // Simulate User A writing a paragraph block into the CRDT doc.
601
+ addBlockToDoc( map, 'block-1', 'Hello world' );
602
+
603
+ // Simulate User B reading the CRDT doc with no local blocks.
604
+ const editedRecord = { blocks: [] } as unknown as Post;
605
+
606
+ const changes = getPostChangesFromCRDTDoc(
607
+ doc,
608
+ editedRecord,
609
+ defaultSyncedProperties
610
+ );
611
+
612
+ const block = ( changes.blocks as any[] )?.[ 0 ];
613
+ expect( block ).toBeDefined();
614
+ expect( block.attributes.content ).toBeInstanceOf( RichTextData );
615
+ expect( block.attributes.content.text ).toBe( 'Hello world' );
616
+ } );
617
+
618
+ it( 'returns nested rich-text in array attributes as RichTextData', () => {
619
+ // Add a table block to the CRDT doc with nested cell content
620
+ // stored as plain strings.
621
+ let blocks = map.get( 'blocks' );
622
+ if ( ! ( blocks instanceof Y.Array ) ) {
623
+ blocks = new Y.Array< YBlock >();
624
+ map.set( 'blocks', blocks );
625
+ }
626
+
627
+ const tableBlock = createYMap< YBlockRecord >();
628
+ tableBlock.set( 'name', 'core/table' );
629
+ tableBlock.set( 'clientId', 'table-1' );
630
+ const attrs = new Y.Map();
631
+ attrs.set( 'body', [
632
+ {
633
+ cells: [
634
+ { content: '<strong>Cell</strong>', tag: 'td' },
635
+ { content: 'Plain', tag: 'td' },
636
+ ],
637
+ },
638
+ ] );
639
+ tableBlock.set( 'attributes', attrs );
640
+ tableBlock.set( 'innerBlocks', new Y.Array() );
641
+ ( blocks as YBlocks ).push( [ tableBlock ] );
642
+
643
+ const editedRecord = { blocks: [] } as unknown as Post;
644
+
645
+ const changes = getPostChangesFromCRDTDoc(
646
+ doc,
647
+ editedRecord,
648
+ defaultSyncedProperties
649
+ );
650
+
651
+ const block = ( changes.blocks as any[] )?.[ 0 ];
652
+ expect( block ).toBeDefined();
653
+
654
+ const cell = block.attributes.body[ 0 ].cells[ 0 ];
655
+ expect( cell.content ).toBeInstanceOf( RichTextData );
656
+ expect( ( cell.content as RichTextData ).toHTMLString() ).toBe(
657
+ '<strong>Cell</strong>'
658
+ );
659
+ } );
660
+
489
661
  it( 'includes undefined blocks in changes', () => {
490
662
  map.set( 'blocks', undefined );
491
663
 
@@ -502,7 +674,7 @@ describe( 'crdt', () => {
502
674
  const changes = getPostChangesFromCRDTDoc(
503
675
  doc,
504
676
  editedRecord,
505
- mockPostType
677
+ defaultSyncedProperties
506
678
  );
507
679
 
508
680
  expect( changes ).toHaveProperty( 'blocks' );
@@ -519,7 +691,7 @@ describe( 'crdt', () => {
519
691
  const changes = getPostChangesFromCRDTDoc(
520
692
  doc,
521
693
  editedRecord,
522
- mockPostType
694
+ defaultSyncedProperties
523
695
  );
524
696
 
525
697
  expect( changes.content ).toBe( 'New content' );
@@ -535,7 +707,7 @@ describe( 'crdt', () => {
535
707
  const changes = getPostChangesFromCRDTDoc(
536
708
  doc,
537
709
  editedRecord,
538
- mockPostType
710
+ defaultSyncedProperties
539
711
  );
540
712
 
541
713
  expect( changes.content ).toBe( 'New content' );
@@ -551,7 +723,7 @@ describe( 'crdt', () => {
551
723
  const changes = getPostChangesFromCRDTDoc(
552
724
  doc,
553
725
  editedRecord,
554
- mockPostType
726
+ defaultSyncedProperties
555
727
  );
556
728
 
557
729
  expect( changes ).not.toHaveProperty( 'content' );
@@ -571,7 +743,7 @@ describe( 'crdt', () => {
571
743
  const changes = getPostChangesFromCRDTDoc(
572
744
  doc,
573
745
  editedRecord,
574
- mockPostType
746
+ defaultSyncedProperties
575
747
  );
576
748
 
577
749
  expect( changes.meta ).toEqual( {
@@ -593,7 +765,7 @@ describe( 'crdt', () => {
593
765
  const changes = getPostChangesFromCRDTDoc(
594
766
  doc,
595
767
  editedRecord,
596
- mockPostType
768
+ defaultSyncedProperties
597
769
  );
598
770
 
599
771
  expect( changes.meta ).toEqual( {
@@ -601,13 +773,34 @@ describe( 'crdt', () => {
601
773
  } );
602
774
  } );
603
775
 
776
+ it( 'excludes orphaned meta keys not present on the edited record', () => {
777
+ // If post meta is registered, saved (landing in a CRDT doc),
778
+ // then unregistered, it can permanently mark the record dirty.
779
+ // Orphaned values should not show up as a change.
780
+ const metaMap = createYMap();
781
+ metaMap.set( 'registered_meta', 'value' );
782
+ metaMap.set( 'orphaned_meta', 'stale value' );
783
+ map.set( 'meta', metaMap );
784
+
785
+ const editedRecord = {
786
+ meta: {
787
+ registered_meta: 'value',
788
+ },
789
+ } as unknown as Post;
790
+
791
+ const changes = getPostChangesFromCRDTDoc(
792
+ doc,
793
+ editedRecord,
794
+ defaultSyncedProperties
795
+ );
796
+
797
+ expect( changes ).not.toHaveProperty( 'meta' );
798
+ } );
799
+
604
800
  it( 'excludes disallowed meta keys in changes', () => {
605
801
  const metaMap = createYMap();
606
802
  metaMap.set( 'public_meta', 'new value' );
607
- metaMap.set(
608
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
609
- 'exclude me'
610
- );
803
+ metaMap.set( POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE, 'exclude me' );
611
804
  map.set( 'meta', metaMap );
612
805
 
613
806
  const editedRecord = {
@@ -619,15 +812,185 @@ describe( 'crdt', () => {
619
812
  const changes = getPostChangesFromCRDTDoc(
620
813
  doc,
621
814
  editedRecord,
622
- mockPostType
815
+ defaultSyncedProperties
623
816
  );
624
817
 
625
818
  expect( changes.meta ).toEqual( {
626
819
  public_meta: 'new value', // from CRDT
627
820
  } );
628
821
  expect( changes.meta ).not.toHaveProperty(
629
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE
822
+ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE
630
823
  );
631
824
  } );
825
+
826
+ it( 'returns taxonomy rest_base changes when in syncedProperties', () => {
827
+ map.set( 'categories', [ 1, 2 ] );
828
+ map.set( 'genre', [ 10, 20 ] );
829
+ map.set( 'tags', [ 3 ] );
830
+
831
+ const editedRecord = {
832
+ categories: [ 1 ],
833
+ genre: [ 10 ], // should be ignored
834
+ tags: [],
835
+ } as unknown as Post;
836
+
837
+ const changes = getPostChangesFromCRDTDoc(
838
+ doc,
839
+ editedRecord,
840
+ defaultSyncedProperties
841
+ ) as Record< string, unknown >;
842
+
843
+ expect( changes.categories ).toEqual( [ 1, 2 ] );
844
+ expect( changes.genre ).toBeUndefined();
845
+ expect( changes.tags ).toEqual( [ 3 ] );
846
+
847
+ const customSyncedProperties = new Set( [
848
+ ...defaultSyncedProperties,
849
+ 'genre', // now included
850
+ ] );
851
+
852
+ const changes2 = getPostChangesFromCRDTDoc(
853
+ doc,
854
+ editedRecord,
855
+ customSyncedProperties
856
+ ) as Record< string, unknown >;
857
+
858
+ expect( changes2.categories ).toEqual( [ 1, 2 ] );
859
+ expect( changes2.genre ).toEqual( [ 10, 20 ] );
860
+ expect( changes2.tags ).toEqual( [ 3 ] );
861
+ } );
862
+
863
+ describe( 'selection recalculation', () => {
864
+ it( 'includes recalculated selection when text is inserted before cursor', () => {
865
+ const ytext = addBlockToDoc( map, 'block-1', 'Hello world' );
866
+
867
+ // Record a selection at offset 5 (cursor between "Hello" and " world").
868
+ updateSelectionHistory( doc, {
869
+ selectionStart: {
870
+ clientId: 'block-1',
871
+ attributeKey: 'content',
872
+ offset: 5,
873
+ },
874
+ selectionEnd: {
875
+ clientId: 'block-1',
876
+ attributeKey: 'content',
877
+ offset: 5,
878
+ },
879
+ } );
880
+
881
+ // Simulate remote insertion: insert "XXX" at position 0.
882
+ ytext.insert( 0, 'XXX' );
883
+
884
+ const editedRecord = {
885
+ title: 'CRDT Title',
886
+ status: 'draft',
887
+ blocks: [],
888
+ } as unknown as Post;
889
+
890
+ const changes = getPostChangesFromCRDTDoc(
891
+ doc,
892
+ editedRecord,
893
+ defaultSyncedProperties
894
+ );
895
+
896
+ expect( changes.selection ).toBeDefined();
897
+ expect( changes.selection?.selectionStart.offset ).toBe( 8 ); // 5 + 3
898
+ expect( changes.selection?.selectionStart.clientId ).toBe(
899
+ 'block-1'
900
+ );
901
+ expect( changes.selection?.selectionStart.attributeKey ).toBe(
902
+ 'content'
903
+ );
904
+ expect( changes.selection?.selectionEnd.offset ).toBe( 8 );
905
+ } );
906
+
907
+ it( 'includes recalculated selection when text is deleted before cursor', () => {
908
+ const ytext = addBlockToDoc( map, 'block-1', 'Hello world' );
909
+
910
+ // Record a selection at offset 8 (cursor between "Hello wo" and "rld").
911
+ updateSelectionHistory( doc, {
912
+ selectionStart: {
913
+ clientId: 'block-1',
914
+ attributeKey: 'content',
915
+ offset: 8,
916
+ },
917
+ selectionEnd: {
918
+ clientId: 'block-1',
919
+ attributeKey: 'content',
920
+ offset: 8,
921
+ },
922
+ } );
923
+
924
+ // Simulate remote deletion: delete "Hello" (5 chars at position 0).
925
+ ytext.delete( 0, 5 );
926
+
927
+ const editedRecord = {
928
+ title: 'CRDT Title',
929
+ status: 'draft',
930
+ blocks: [],
931
+ } as unknown as Post;
932
+
933
+ const changes = getPostChangesFromCRDTDoc(
934
+ doc,
935
+ editedRecord,
936
+ defaultSyncedProperties
937
+ );
938
+
939
+ expect( changes.selection ).toBeDefined();
940
+ expect( changes.selection?.selectionStart.offset ).toBe( 3 ); // 8 - 5
941
+ } );
942
+
943
+ it( 'does not include selection when selection history is empty', () => {
944
+ addBlockToDoc( map, 'block-1', 'Hello world' );
945
+
946
+ const editedRecord = {
947
+ title: 'CRDT Title',
948
+ status: 'draft',
949
+ blocks: [],
950
+ } as unknown as Post;
951
+
952
+ const changes = getPostChangesFromCRDTDoc(
953
+ doc,
954
+ editedRecord,
955
+ defaultSyncedProperties
956
+ );
957
+
958
+ expect( changes.selection ).toBeUndefined();
959
+ } );
960
+ } );
632
961
  } );
633
962
  } );
963
+
964
+ /**
965
+ * Helper to create a block with a Y.Text content attribute
966
+ * in the CRDT document.
967
+ *
968
+ * @param map
969
+ * @param clientId Block client ID.
970
+ * @param content Initial text content.
971
+ * @param name Block name (default: 'core/paragraph').
972
+ */
973
+ function addBlockToDoc(
974
+ map: YMapWrap< YPostRecord >,
975
+ clientId: string,
976
+ content: string,
977
+ name = 'core/paragraph'
978
+ ): Y.Text {
979
+ let blocks = map.get( 'blocks' );
980
+ if ( ! ( blocks instanceof Y.Array ) ) {
981
+ blocks = new Y.Array< YBlock >();
982
+ map.set( 'blocks', blocks );
983
+ }
984
+
985
+ const block = createYMap< YBlockRecord >();
986
+ block.set( 'name', name );
987
+ block.set( 'clientId', clientId );
988
+ const attrs = new Y.Map();
989
+ const ytext = new Y.Text( content );
990
+ attrs.set( 'content', ytext );
991
+ block.set( 'attributes', attrs );
992
+ block.set( 'innerBlocks', new Y.Array() );
993
+ ( blocks as YBlocks ).push( [ block ] );
994
+
995
+ return ytext;
996
+ }