@wordpress/core-data 7.45.0 → 7.45.1-next.v.202605131032.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 (181) hide show
  1. package/build/actions.cjs +8 -6
  2. package/build/actions.cjs.map +2 -2
  3. package/build/awareness/post-editor-awareness.cjs +1 -1
  4. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  5. package/build/resolvers.cjs +2 -1
  6. package/build/resolvers.cjs.map +2 -2
  7. package/build/types.cjs.map +2 -2
  8. package/build/utils/block-selection-history.cjs +4 -1
  9. package/build/utils/block-selection-history.cjs.map +2 -2
  10. package/build/utils/crdt-blocks.cjs +157 -89
  11. package/build/utils/crdt-blocks.cjs.map +2 -2
  12. package/build/utils/crdt-selection.cjs +1 -1
  13. package/build/utils/crdt-selection.cjs.map +2 -2
  14. package/build/utils/crdt-user-selections.cjs +4 -1
  15. package/build/utils/crdt-user-selections.cjs.map +2 -2
  16. package/build/utils/crdt-utils.cjs +18 -6
  17. package/build/utils/crdt-utils.cjs.map +2 -2
  18. package/build/utils/crdt.cjs +12 -2
  19. package/build/utils/crdt.cjs.map +2 -2
  20. package/build-module/actions.mjs +8 -6
  21. package/build-module/actions.mjs.map +2 -2
  22. package/build-module/awareness/post-editor-awareness.mjs +5 -2
  23. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  24. package/build-module/resolvers.mjs +2 -1
  25. package/build-module/resolvers.mjs.map +2 -2
  26. package/build-module/types.mjs.map +2 -2
  27. package/build-module/utils/block-selection-history.mjs +5 -1
  28. package/build-module/utils/block-selection-history.mjs.map +2 -2
  29. package/build-module/utils/crdt-blocks.mjs +162 -90
  30. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  31. package/build-module/utils/crdt-selection.mjs +2 -1
  32. package/build-module/utils/crdt-selection.mjs.map +2 -2
  33. package/build-module/utils/crdt-user-selections.mjs +9 -2
  34. package/build-module/utils/crdt-user-selections.mjs.map +2 -2
  35. package/build-module/utils/crdt-utils.mjs +16 -6
  36. package/build-module/utils/crdt-utils.mjs.map +2 -2
  37. package/build-module/utils/crdt.mjs +13 -2
  38. package/build-module/utils/crdt.mjs.map +2 -2
  39. package/build-types/actions.d.ts +177 -64
  40. package/build-types/actions.d.ts.map +1 -1
  41. package/build-types/awareness/awareness-state.d.ts.map +1 -1
  42. package/build-types/awareness/base-awareness.d.ts +0 -3
  43. package/build-types/awareness/base-awareness.d.ts.map +1 -1
  44. package/build-types/awareness/post-editor-awareness.d.ts +1 -8
  45. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  46. package/build-types/awareness/typed-awareness.d.ts.map +1 -1
  47. package/build-types/batch/create-batch.d.ts +1 -1
  48. package/build-types/batch/create-batch.d.ts.map +1 -1
  49. package/build-types/batch/default-processor.d.ts.map +1 -1
  50. package/build-types/batch/index.d.ts +2 -2
  51. package/build-types/batch/index.d.ts.map +1 -1
  52. package/build-types/entities.d.ts +114 -87
  53. package/build-types/entities.d.ts.map +1 -1
  54. package/build-types/entity-context.d.ts +1 -1
  55. package/build-types/entity-context.d.ts.map +1 -1
  56. package/build-types/entity-provider.d.ts +2 -2
  57. package/build-types/entity-provider.d.ts.map +1 -1
  58. package/build-types/entity-types/attachment.d.ts.map +1 -1
  59. package/build-types/entity-types/base-entity-records.d.ts.map +1 -1
  60. package/build-types/entity-types/base.d.ts.map +1 -1
  61. package/build-types/entity-types/comment.d.ts.map +1 -1
  62. package/build-types/entity-types/font-collection.d.ts.map +1 -1
  63. package/build-types/entity-types/font-family.d.ts.map +1 -1
  64. package/build-types/entity-types/global-styles-revision.d.ts.map +1 -1
  65. package/build-types/entity-types/icon.d.ts.map +1 -1
  66. package/build-types/entity-types/menu-location.d.ts.map +1 -1
  67. package/build-types/entity-types/nav-menu-item.d.ts.map +1 -1
  68. package/build-types/entity-types/nav-menu.d.ts.map +1 -1
  69. package/build-types/entity-types/page.d.ts.map +1 -1
  70. package/build-types/entity-types/plugin.d.ts.map +1 -1
  71. package/build-types/entity-types/post-revision.d.ts.map +1 -1
  72. package/build-types/entity-types/post-status.d.ts.map +1 -1
  73. package/build-types/entity-types/post.d.ts.map +1 -1
  74. package/build-types/entity-types/settings.d.ts.map +1 -1
  75. package/build-types/entity-types/sidebar.d.ts.map +1 -1
  76. package/build-types/entity-types/taxonomy.d.ts.map +1 -1
  77. package/build-types/entity-types/term.d.ts.map +1 -1
  78. package/build-types/entity-types/theme.d.ts.map +1 -1
  79. package/build-types/entity-types/type.d.ts.map +1 -1
  80. package/build-types/entity-types/user.d.ts.map +1 -1
  81. package/build-types/entity-types/widget-type.d.ts.map +1 -1
  82. package/build-types/entity-types/widget.d.ts.map +1 -1
  83. package/build-types/entity-types/wp-template-part.d.ts.map +1 -1
  84. package/build-types/entity-types/wp-template.d.ts.map +1 -1
  85. package/build-types/fetch/__experimental-fetch-url-data.d.ts +2 -5
  86. package/build-types/fetch/__experimental-fetch-url-data.d.ts.map +1 -1
  87. package/build-types/fetch/index.d.ts +3 -3
  88. package/build-types/fetch/index.d.ts.map +1 -1
  89. package/build-types/footnotes/get-footnotes-order.d.ts.map +1 -1
  90. package/build-types/footnotes/get-rich-text-values-cached.d.ts.map +1 -1
  91. package/build-types/footnotes/index.d.ts +1 -1
  92. package/build-types/footnotes/index.d.ts.map +1 -1
  93. package/build-types/hooks/use-entity-block-editor.d.ts +1 -1
  94. package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -1
  95. package/build-types/hooks/use-entity-id.d.ts.map +1 -1
  96. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  97. package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
  98. package/build-types/index.d.ts +155 -153
  99. package/build-types/index.d.ts.map +1 -1
  100. package/build-types/locks/actions.d.ts +1 -1
  101. package/build-types/locks/actions.d.ts.map +1 -1
  102. package/build-types/locks/engine.d.ts +1 -1
  103. package/build-types/locks/engine.d.ts.map +1 -1
  104. package/build-types/locks/reducer.d.ts.map +1 -1
  105. package/build-types/locks/selectors.d.ts +2 -2
  106. package/build-types/locks/selectors.d.ts.map +1 -1
  107. package/build-types/locks/utils.d.ts +5 -5
  108. package/build-types/locks/utils.d.ts.map +1 -1
  109. package/build-types/name.d.ts +1 -1
  110. package/build-types/name.d.ts.map +1 -1
  111. package/build-types/private-actions.d.ts +45 -29
  112. package/build-types/private-actions.d.ts.map +1 -1
  113. package/build-types/private-apis.d.ts +1 -1
  114. package/build-types/private-apis.d.ts.map +1 -1
  115. package/build-types/queried-data/actions.d.ts +3 -3
  116. package/build-types/queried-data/actions.d.ts.map +1 -1
  117. package/build-types/queried-data/get-query-parts.d.ts +10 -34
  118. package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
  119. package/build-types/queried-data/index.d.ts +3 -3
  120. package/build-types/queried-data/index.d.ts.map +1 -1
  121. package/build-types/queried-data/reducer.d.ts +7 -23
  122. package/build-types/queried-data/reducer.d.ts.map +1 -1
  123. package/build-types/queried-data/selectors.d.ts +3 -3
  124. package/build-types/queried-data/selectors.d.ts.map +1 -1
  125. package/build-types/reducer.d.ts +40 -32
  126. package/build-types/reducer.d.ts.map +1 -1
  127. package/build-types/resolvers.d.ts +130 -47
  128. package/build-types/resolvers.d.ts.map +1 -1
  129. package/build-types/selectors.d.ts +1 -1
  130. package/build-types/selectors.d.ts.map +1 -1
  131. package/build-types/types.d.ts +61 -6
  132. package/build-types/types.d.ts.map +1 -1
  133. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  134. package/build-types/utils/conservative-map-item.d.ts.map +1 -1
  135. package/build-types/utils/crdt-blocks.d.ts +19 -9
  136. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  137. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  138. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  139. package/build-types/utils/crdt-utils.d.ts +35 -2
  140. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  141. package/build-types/utils/crdt.d.ts.map +1 -1
  142. package/build-types/utils/forward-resolver.d.ts +2 -2
  143. package/build-types/utils/forward-resolver.d.ts.map +1 -1
  144. package/build-types/utils/get-nested-value.d.ts.map +1 -1
  145. package/build-types/utils/get-normalized-comma-separable.d.ts +1 -1
  146. package/build-types/utils/get-normalized-comma-separable.d.ts.map +1 -1
  147. package/build-types/utils/if-matching-action.d.ts +3 -3
  148. package/build-types/utils/if-matching-action.d.ts.map +1 -1
  149. package/build-types/utils/index.d.ts +12 -12
  150. package/build-types/utils/index.d.ts.map +1 -1
  151. package/build-types/utils/is-numeric-id.d.ts.map +1 -1
  152. package/build-types/utils/log-entity-deprecation.d.ts +1 -1
  153. package/build-types/utils/log-entity-deprecation.d.ts.map +1 -1
  154. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -1
  155. package/build-types/utils/receive-intermediate-results.d.ts +1 -1
  156. package/build-types/utils/receive-intermediate-results.d.ts.map +1 -1
  157. package/build-types/utils/replace-action.d.ts +3 -3
  158. package/build-types/utils/replace-action.d.ts.map +1 -1
  159. package/build-types/utils/set-nested-value.d.ts.map +1 -1
  160. package/build-types/utils/user-permissions.d.ts +3 -3
  161. package/build-types/utils/user-permissions.d.ts.map +1 -1
  162. package/build-types/utils/with-weak-map-cache.d.ts +1 -1
  163. package/build-types/utils/with-weak-map-cache.d.ts.map +1 -1
  164. package/package.json +20 -20
  165. package/src/actions.js +7 -9
  166. package/src/awareness/post-editor-awareness.ts +5 -2
  167. package/src/resolvers.js +2 -1
  168. package/src/test/actions.js +58 -0
  169. package/src/test/resolvers.js +115 -2
  170. package/src/test/rtc-rich-text-offset-space.test.js +204 -0
  171. package/src/types.ts +63 -6
  172. package/src/utils/block-selection-history.ts +5 -1
  173. package/src/utils/crdt-blocks.ts +316 -116
  174. package/src/utils/crdt-selection.ts +2 -1
  175. package/src/utils/crdt-user-selections.ts +9 -2
  176. package/src/utils/crdt-utils.ts +53 -10
  177. package/src/utils/crdt.ts +30 -4
  178. package/src/utils/test/crdt-blocks.ts +74 -18
  179. package/src/utils/test/crdt-utils.ts +18 -2
  180. package/src/utils/test/rtc-rich-text-cursor-scope.test.js +267 -0
  181. package/src/utils/test/rtc-rich-text-offset-space.test.js +469 -0
@@ -0,0 +1,469 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { beforeEach, describe, expect, it, jest } from '@jest/globals';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { Y } from '@wordpress/sync';
10
+
11
+ /**
12
+ * Mock block schemas and sync providers.
13
+ */
14
+ jest.mock( '@wordpress/blocks', () => {
15
+ const actual = jest.requireActual( '@wordpress/blocks' );
16
+
17
+ return {
18
+ ...actual,
19
+ getBlockTypes: () => [
20
+ {
21
+ name: 'core/paragraph',
22
+ attributes: {
23
+ content: { type: 'rich-text' },
24
+ },
25
+ },
26
+ ],
27
+ };
28
+ } );
29
+
30
+ jest.mock( '../../../../sync/src/providers', () => ( {
31
+ getProviderCreators: jest.fn(),
32
+ } ) );
33
+
34
+ /**
35
+ * Internal dependencies
36
+ */
37
+ import { createSyncManager } from '../../../../sync/src/manager';
38
+ import { getProviderCreators } from '../../../../sync/src/providers';
39
+ import { CRDT_RECORD_MAP_KEY, Delta } from '../../sync';
40
+ import { applyPostChangesToCRDTDoc } from '../crdt';
41
+ import { mergeCrdtBlocks } from '../crdt-blocks';
42
+ import { getRootMap, richTextOffsetToHtmlIndex } from '../crdt-utils';
43
+
44
+ const mockGetProviderCreators = jest.mocked( getProviderCreators );
45
+
46
+ const SYNCED_PROPERTIES = new Set( [ 'blocks' ] );
47
+ const OLD_HTML = '<em>italic</em><em>italic</em>';
48
+ const NEW_HTML = '<em>italic</em>beta';
49
+ const TEXT_OFFSET = 10;
50
+ const OLD_AMBIGUOUS_HTML = '<em>a</em>';
51
+ const NEW_AMBIGUOUS_HTML = '<em>aa</em>';
52
+ const AMBIGUOUS_TEXT_OFFSET = 1;
53
+ const FUZZ_SEED = 0x5eed1234;
54
+ const FUZZ_CASE_COUNT = 500;
55
+ const PLACEMENT_FUZZ_CASE_COUNT = 500;
56
+ const FUZZ_CHARS = [ 'a', 'a', 'a', 'b', 'b', 'c', 'x', 'y' ];
57
+ const FUZZ_WRAPPERS = [
58
+ ( text ) => text,
59
+ ( text ) => `<em>${ text }</em>`,
60
+ ( text ) => `<strong>${ text }</strong>`,
61
+ ( text ) => `<strong><em>${ text }</em></strong>`,
62
+ ( text ) => `<a href="https://example.com">${ text }</a>`,
63
+ ];
64
+
65
+ function makeParagraphBlock( content ) {
66
+ return {
67
+ clientId: 'block-1',
68
+ name: 'core/paragraph',
69
+ attributes: { content },
70
+ innerBlocks: [],
71
+ };
72
+ }
73
+
74
+ function getSelection() {
75
+ return {
76
+ selectionStart: {
77
+ clientId: 'block-1',
78
+ attributeKey: 'content',
79
+ offset: TEXT_OFFSET,
80
+ },
81
+ selectionEnd: {
82
+ clientId: 'block-1',
83
+ attributeKey: 'content',
84
+ offset: TEXT_OFFSET,
85
+ },
86
+ };
87
+ }
88
+
89
+ function getFirstBlockContentYText( yblocks ) {
90
+ const attrs = yblocks.get( 0 ).get( 'attributes' );
91
+ return attrs.get( 'content' );
92
+ }
93
+
94
+ function readFirstBlockContentFromYBlocks( yblocks ) {
95
+ return getFirstBlockContentYText( yblocks ).toString();
96
+ }
97
+
98
+ function readFirstBlockContentFromDoc( ydoc ) {
99
+ const ymap = getRootMap( ydoc, CRDT_RECORD_MAP_KEY );
100
+ const yblocks = ymap.get( 'blocks' );
101
+ return readFirstBlockContentFromYBlocks( yblocks );
102
+ }
103
+
104
+ function waitForNextTick() {
105
+ return new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
106
+ }
107
+
108
+ function createRandom( seed ) {
109
+ const modulus = 2147483647;
110
+ const multiplier = 48271;
111
+ let state = seed % modulus;
112
+
113
+ if ( state <= 0 ) {
114
+ state += modulus - 1;
115
+ }
116
+
117
+ return () => {
118
+ state = ( state * multiplier ) % modulus;
119
+ return state / modulus;
120
+ };
121
+ }
122
+
123
+ function randomInt( random, min, max ) {
124
+ return min + Math.floor( random() * ( max - min + 1 ) );
125
+ }
126
+
127
+ function randomItem( random, items ) {
128
+ return items[ randomInt( random, 0, items.length - 1 ) ];
129
+ }
130
+
131
+ function randomText( random, minLength, maxLength ) {
132
+ const length = randomInt( random, minLength, maxLength );
133
+ let text = '';
134
+
135
+ for ( let i = 0; i < length; i++ ) {
136
+ text += randomItem( random, FUZZ_CHARS );
137
+ }
138
+
139
+ return text;
140
+ }
141
+
142
+ function makeRandomRichTextHtml( random, text ) {
143
+ let html = '';
144
+ let offset = 0;
145
+
146
+ while ( offset < text.length ) {
147
+ const chunkLength = randomInt(
148
+ random,
149
+ 1,
150
+ Math.min( 3, text.length - offset )
151
+ );
152
+ const chunk = text.slice( offset, offset + chunkLength );
153
+ html += randomItem( random, FUZZ_WRAPPERS )( chunk );
154
+ offset += chunkLength;
155
+ }
156
+
157
+ return html;
158
+ }
159
+
160
+ function makeCursor( offset ) {
161
+ return {
162
+ attributeKey: 'content',
163
+ clientId: 'block-1',
164
+ offset,
165
+ };
166
+ }
167
+
168
+ function deltaForUpdate( oldHtml, newHtml, cursorPosition ) {
169
+ return new Delta( [ { insert: oldHtml } ] ).diffWithCursor(
170
+ new Delta( [ { insert: newHtml } ] ),
171
+ cursorPosition
172
+ );
173
+ }
174
+
175
+ function applyDeltaToString( oldHtml, delta ) {
176
+ return new Delta( [ { insert: oldHtml } ] )
177
+ .compose( delta )
178
+ .ops.map( ( op ) => ( typeof op.insert === 'string' ? op.insert : '' ) )
179
+ .join( '' );
180
+ }
181
+
182
+ function areEqual( actual, expected ) {
183
+ return JSON.stringify( actual ) === JSON.stringify( expected );
184
+ }
185
+
186
+ function assertEqualWithContext( actual, expected, context ) {
187
+ if ( areEqual( actual, expected ) ) {
188
+ return;
189
+ }
190
+
191
+ throw new Error(
192
+ [
193
+ 'Unexpected fuzz result.',
194
+ `Expected: ${ JSON.stringify( expected ) }`,
195
+ `Actual: ${ JSON.stringify( actual ) }`,
196
+ `Context: ${ JSON.stringify( context ) }`,
197
+ ].join( '\n' )
198
+ );
199
+ }
200
+
201
+ describe( 'RTC rich-text offset-space bug', () => {
202
+ beforeEach( () => {
203
+ jest.clearAllMocks();
204
+ mockGetProviderCreators.mockReturnValue( [
205
+ jest.fn( async () => ( {
206
+ destroy: jest.fn(),
207
+ on: jest.fn(),
208
+ } ) ),
209
+ ] );
210
+ } );
211
+
212
+ it( 'preserves formatted paragraph content in mergeCrdtBlocks', () => {
213
+ const doc = new Y.Doc();
214
+ const yblocks = doc.getArray( 'blocks' );
215
+
216
+ mergeCrdtBlocks( yblocks, [ makeParagraphBlock( OLD_HTML ) ], null );
217
+ mergeCrdtBlocks(
218
+ yblocks,
219
+ [ makeParagraphBlock( NEW_HTML ) ],
220
+ makeCursor( TEXT_OFFSET )
221
+ );
222
+
223
+ expect( readFirstBlockContentFromYBlocks( yblocks ) ).toBe( NEW_HTML );
224
+ } );
225
+
226
+ it( 'uses the updated rich-text HTML to place cursor-guided inserts', () => {
227
+ const doc = new Y.Doc();
228
+ const yblocks = doc.getArray( 'blocks' );
229
+ const deltas = [];
230
+
231
+ mergeCrdtBlocks(
232
+ yblocks,
233
+ [ makeParagraphBlock( OLD_AMBIGUOUS_HTML ) ],
234
+ null
235
+ );
236
+
237
+ const content = getFirstBlockContentYText( yblocks );
238
+ content.observe( ( event ) => {
239
+ deltas.push( event.delta );
240
+ } );
241
+
242
+ mergeCrdtBlocks(
243
+ yblocks,
244
+ [ makeParagraphBlock( NEW_AMBIGUOUS_HTML ) ],
245
+ {
246
+ attributeKey: 'content',
247
+ clientId: 'block-1',
248
+ offset: AMBIGUOUS_TEXT_OFFSET,
249
+ }
250
+ );
251
+
252
+ expect( content.toString() ).toBe( NEW_AMBIGUOUS_HTML );
253
+ // Converting offset 1 through the old HTML would retain 5 and insert
254
+ // after "a", which produces the same final string but the wrong edit.
255
+ expect( deltas ).toEqual( [ [ { retain: 4 }, { insert: 'a' } ] ] );
256
+ } );
257
+
258
+ it( 'fuzzes formatted rich-text updates without content corruption', () => {
259
+ const random = createRandom( FUZZ_SEED );
260
+ let checkedCases = 0;
261
+
262
+ for ( let i = 0; i < FUZZ_CASE_COUNT; i++ ) {
263
+ const oldText = randomText( random, 1, 10 );
264
+ const newText = randomText( random, 1, 10 );
265
+ const oldHtml = makeRandomRichTextHtml( random, oldText );
266
+ const newHtml = makeRandomRichTextHtml( random, newText );
267
+ const cursorOffset = randomInt( random, 0, newText.length );
268
+ const doc = new Y.Doc();
269
+ const yblocks = doc.getArray( 'blocks' );
270
+
271
+ mergeCrdtBlocks( yblocks, [ makeParagraphBlock( oldHtml ) ], null );
272
+ mergeCrdtBlocks(
273
+ yblocks,
274
+ [ makeParagraphBlock( newHtml ) ],
275
+ makeCursor( cursorOffset )
276
+ );
277
+
278
+ assertEqualWithContext(
279
+ readFirstBlockContentFromYBlocks( yblocks ),
280
+ newHtml,
281
+ {
282
+ seed: FUZZ_SEED,
283
+ caseIndex: i,
284
+ oldHtml,
285
+ newHtml,
286
+ cursorOffset,
287
+ }
288
+ );
289
+
290
+ checkedCases++;
291
+ doc.destroy();
292
+ }
293
+
294
+ expect( checkedCases ).toBe( FUZZ_CASE_COUNT );
295
+ } );
296
+
297
+ it( 'fuzzes cursor-guided deltas against updated HTML cursor indices', () => {
298
+ const random = createRandom( FUZZ_SEED + 1 );
299
+ let attempts = 0;
300
+ let checkedCases = 0;
301
+ let differentCursorIndexCases = 0;
302
+ let oldHtmlWouldPlaceDifferentDeltaCases = 0;
303
+
304
+ while ( checkedCases < PLACEMENT_FUZZ_CASE_COUNT ) {
305
+ attempts++;
306
+ const oldText = randomText( random, 1, 10 );
307
+ const insertAt = randomInt( random, 0, oldText.length );
308
+ const insertedText =
309
+ oldText[ insertAt ] ??
310
+ oldText[ insertAt - 1 ] ??
311
+ randomItem( random, FUZZ_CHARS );
312
+ const newText =
313
+ oldText.slice( 0, insertAt ) +
314
+ insertedText +
315
+ oldText.slice( insertAt );
316
+ const cursorOffset = insertAt + insertedText.length;
317
+ const oldHtml = makeRandomRichTextHtml( random, oldText );
318
+ const newHtml = makeRandomRichTextHtml( random, newText );
319
+ const expectedCursorPosition = richTextOffsetToHtmlIndex(
320
+ newHtml,
321
+ cursorOffset
322
+ );
323
+ const oldHtmlCursorPosition = richTextOffsetToHtmlIndex(
324
+ oldHtml,
325
+ cursorOffset
326
+ );
327
+ const expectedDelta = deltaForUpdate(
328
+ oldHtml,
329
+ newHtml,
330
+ expectedCursorPosition
331
+ );
332
+ const oldHtmlDelta = deltaForUpdate(
333
+ oldHtml,
334
+ newHtml,
335
+ oldHtmlCursorPosition
336
+ );
337
+
338
+ if ( applyDeltaToString( oldHtml, expectedDelta ) !== newHtml ) {
339
+ continue;
340
+ }
341
+
342
+ if ( expectedCursorPosition !== oldHtmlCursorPosition ) {
343
+ differentCursorIndexCases++;
344
+ }
345
+
346
+ if ( ! areEqual( expectedDelta.ops, oldHtmlDelta.ops ) ) {
347
+ oldHtmlWouldPlaceDifferentDeltaCases++;
348
+ }
349
+
350
+ const doc = new Y.Doc();
351
+ const yblocks = doc.getArray( 'blocks' );
352
+
353
+ mergeCrdtBlocks( yblocks, [ makeParagraphBlock( oldHtml ) ], null );
354
+
355
+ const content = getFirstBlockContentYText( yblocks );
356
+ const deltas = [];
357
+ content.observe( ( event ) => {
358
+ deltas.push( event.delta );
359
+ } );
360
+
361
+ mergeCrdtBlocks(
362
+ yblocks,
363
+ [ makeParagraphBlock( newHtml ) ],
364
+ makeCursor( cursorOffset )
365
+ );
366
+
367
+ assertEqualWithContext( content.toString(), newHtml, {
368
+ seed: FUZZ_SEED,
369
+ attempts,
370
+ checkedCases,
371
+ oldHtml,
372
+ newHtml,
373
+ cursorOffset,
374
+ expectedCursorPosition,
375
+ oldHtmlCursorPosition,
376
+ } );
377
+ assertEqualWithContext( deltas, [ expectedDelta.ops ], {
378
+ seed: FUZZ_SEED,
379
+ attempts,
380
+ checkedCases,
381
+ oldHtml,
382
+ newHtml,
383
+ cursorOffset,
384
+ expectedCursorPosition,
385
+ oldHtmlCursorPosition,
386
+ expectedDelta: expectedDelta.ops,
387
+ oldHtmlDelta: oldHtmlDelta.ops,
388
+ } );
389
+
390
+ checkedCases++;
391
+ doc.destroy();
392
+ }
393
+
394
+ expect( checkedCases ).toBe( PLACEMENT_FUZZ_CASE_COUNT );
395
+ expect( attempts ).toBeLessThanOrEqual( PLACEMENT_FUZZ_CASE_COUNT * 2 );
396
+ expect( differentCursorIndexCases ).toBeGreaterThan( 100 );
397
+ expect( oldHtmlWouldPlaceDifferentDeltaCases ).toBeGreaterThan( 25 );
398
+ } );
399
+
400
+ it( 'preserves formatted paragraph content in applyPostChangesToCRDTDoc', () => {
401
+ const doc = new Y.Doc();
402
+
403
+ applyPostChangesToCRDTDoc(
404
+ doc,
405
+ { blocks: [ makeParagraphBlock( OLD_HTML ) ] },
406
+ SYNCED_PROPERTIES
407
+ );
408
+
409
+ applyPostChangesToCRDTDoc(
410
+ doc,
411
+ {
412
+ blocks: [ makeParagraphBlock( NEW_HTML ) ],
413
+ selection: getSelection(),
414
+ },
415
+ SYNCED_PROPERTIES
416
+ );
417
+
418
+ expect( readFirstBlockContentFromDoc( doc ) ).toBe( NEW_HTML );
419
+ } );
420
+
421
+ it( 'preserves formatted paragraph content in SyncManager.update', async () => {
422
+ let capturedDoc;
423
+ const manager = createSyncManager();
424
+ const handlers = {
425
+ addUndoMeta: jest.fn(),
426
+ editRecord: jest.fn(),
427
+ getEditedRecord: jest.fn( async () => ( {
428
+ id: 1,
429
+ blocks: [ makeParagraphBlock( OLD_HTML ) ],
430
+ } ) ),
431
+ onStatusChange: jest.fn(),
432
+ persistCRDTDoc: jest.fn(),
433
+ refetchRecord: jest.fn( async () => {} ),
434
+ restoreUndoMeta: jest.fn(),
435
+ };
436
+ const syncConfig = {
437
+ applyChangesToCRDTDoc: ( ydoc, changes ) => {
438
+ capturedDoc = ydoc;
439
+ applyPostChangesToCRDTDoc( ydoc, changes, SYNCED_PROPERTIES );
440
+ },
441
+ createAwareness: jest.fn(),
442
+ getChangesFromCRDTDoc: jest.fn( () => ( {} ) ),
443
+ getPersistedCRDTDoc: jest.fn( () => null ),
444
+ };
445
+
446
+ await manager.load(
447
+ syncConfig,
448
+ 'postType/post',
449
+ '1',
450
+ { id: 1, blocks: [ makeParagraphBlock( OLD_HTML ) ] },
451
+ handlers
452
+ );
453
+
454
+ manager.update(
455
+ 'postType/post',
456
+ '1',
457
+ {
458
+ blocks: [ makeParagraphBlock( NEW_HTML ) ],
459
+ selection: getSelection(),
460
+ },
461
+ 'LOCAL_EDITOR_ORIGIN'
462
+ );
463
+ await waitForNextTick();
464
+
465
+ expect( readFirstBlockContentFromDoc( capturedDoc ) ).toBe( NEW_HTML );
466
+
467
+ manager.unload( 'postType/post', '1' );
468
+ } );
469
+ } );