@wordpress/core-data 7.48.0 → 7.48.1

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 (99) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/build/awareness/block-lookup.cjs +14 -26
  3. package/build/awareness/block-lookup.cjs.map +2 -2
  4. package/build/awareness/post-editor-awareness.cjs +4 -3
  5. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  6. package/build/entities.cjs +4 -2
  7. package/build/entities.cjs.map +2 -2
  8. package/build/hooks/use-post-editor-awareness-state.cjs +8 -2
  9. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  10. package/build/private-actions.cjs +8 -0
  11. package/build/private-actions.cjs.map +2 -2
  12. package/build/private-selectors.cjs.map +2 -2
  13. package/build/reducer.cjs +13 -0
  14. package/build/reducer.cjs.map +2 -2
  15. package/build/resolvers.cjs +13 -8
  16. package/build/resolvers.cjs.map +2 -2
  17. package/build/selectors.cjs +7 -0
  18. package/build/selectors.cjs.map +2 -2
  19. package/build/utils/crdt-blocks.cjs +12 -2
  20. package/build/utils/crdt-blocks.cjs.map +2 -2
  21. package/build/utils/crdt.cjs +2 -1
  22. package/build/utils/crdt.cjs.map +2 -2
  23. package/build/utils/index.cjs +3 -0
  24. package/build/utils/index.cjs.map +2 -2
  25. package/build/utils/save-crdt-doc.cjs +75 -0
  26. package/build/utils/save-crdt-doc.cjs.map +7 -0
  27. package/build-module/awareness/block-lookup.mjs +13 -26
  28. package/build-module/awareness/block-lookup.mjs.map +2 -2
  29. package/build-module/awareness/post-editor-awareness.mjs +4 -3
  30. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  31. package/build-module/entities.mjs +4 -2
  32. package/build-module/entities.mjs.map +2 -2
  33. package/build-module/hooks/use-post-editor-awareness-state.mjs +9 -3
  34. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  35. package/build-module/private-actions.mjs +7 -0
  36. package/build-module/private-actions.mjs.map +2 -2
  37. package/build-module/private-selectors.mjs.map +2 -2
  38. package/build-module/reducer.mjs +12 -0
  39. package/build-module/reducer.mjs.map +2 -2
  40. package/build-module/resolvers.mjs +15 -9
  41. package/build-module/resolvers.mjs.map +2 -2
  42. package/build-module/selectors.mjs +7 -0
  43. package/build-module/selectors.mjs.map +2 -2
  44. package/build-module/utils/crdt-blocks.mjs +12 -2
  45. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  46. package/build-module/utils/crdt.mjs +2 -1
  47. package/build-module/utils/crdt.mjs.map +2 -2
  48. package/build-module/utils/index.mjs +2 -0
  49. package/build-module/utils/index.mjs.map +2 -2
  50. package/build-module/utils/save-crdt-doc.mjs +40 -0
  51. package/build-module/utils/save-crdt-doc.mjs.map +7 -0
  52. package/build-types/awareness/block-lookup.d.ts +27 -7
  53. package/build-types/awareness/block-lookup.d.ts.map +1 -1
  54. package/build-types/awareness/post-editor-awareness.d.ts +3 -1
  55. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  56. package/build-types/entities.d.ts.map +1 -1
  57. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  58. package/build-types/private-actions.d.ts +15 -0
  59. package/build-types/private-actions.d.ts.map +1 -1
  60. package/build-types/private-selectors.d.ts +0 -12
  61. package/build-types/private-selectors.d.ts.map +1 -1
  62. package/build-types/reducer.d.ts +15 -0
  63. package/build-types/reducer.d.ts.map +1 -1
  64. package/build-types/resolvers.d.ts.map +1 -1
  65. package/build-types/selectors.d.ts +4 -0
  66. package/build-types/selectors.d.ts.map +1 -1
  67. package/build-types/utils/crdt-blocks.d.ts +5 -1
  68. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  69. package/build-types/utils/crdt.d.ts.map +1 -1
  70. package/build-types/utils/index.d.ts +1 -0
  71. package/build-types/utils/index.d.ts.map +1 -1
  72. package/build-types/utils/on-sub-key.d.ts +4 -0
  73. package/build-types/utils/on-sub-key.d.ts.map +1 -0
  74. package/build-types/utils/save-crdt-doc.d.ts +8 -0
  75. package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
  76. package/package.json +22 -20
  77. package/src/awareness/block-lookup.ts +21 -62
  78. package/src/awareness/post-editor-awareness.ts +8 -3
  79. package/src/awareness/test/block-lookup.ts +98 -94
  80. package/src/awareness/test/post-editor-awareness.ts +177 -180
  81. package/src/entities.js +9 -3
  82. package/src/hooks/test/use-post-editor-awareness-state.ts +10 -2
  83. package/src/hooks/use-post-editor-awareness-state.ts +20 -7
  84. package/src/private-actions.js +18 -0
  85. package/src/private-selectors.ts +0 -12
  86. package/src/reducer.js +17 -0
  87. package/src/resolvers.js +20 -13
  88. package/src/selectors.ts +11 -0
  89. package/src/test/private-selectors.js +66 -0
  90. package/src/test/reducer.js +44 -0
  91. package/src/test/resolvers.js +121 -113
  92. package/src/test/selectors.js +48 -0
  93. package/src/utils/crdt-blocks.ts +27 -22
  94. package/src/utils/crdt.ts +2 -1
  95. package/src/utils/index.js +1 -0
  96. package/src/utils/save-crdt-doc.js +64 -0
  97. package/src/utils/test/crdt-blocks.ts +57 -2
  98. package/src/utils/test/rtc-rich-text-cursor-scope.test.js +2 -2
  99. package/src/utils/test/save-crdt-doc.js +185 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/core-data",
3
- "version": "7.48.0",
3
+ "version": "7.48.1",
4
4
  "description": "Access to and manipulation of core WordPress entities.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -40,7 +40,6 @@
40
40
  },
41
41
  "./package.json": "./package.json"
42
42
  },
43
- "react-native": "src/index",
44
43
  "wpScript": true,
45
44
  "types": "build-types",
46
45
  "sideEffects": [
@@ -49,22 +48,23 @@
49
48
  "build-module/index.mjs"
50
49
  ],
51
50
  "dependencies": {
52
- "@wordpress/api-fetch": "^7.48.0",
53
- "@wordpress/block-editor": "^15.21.0",
54
- "@wordpress/blocks": "^15.21.0",
55
- "@wordpress/compose": "^8.1.0",
56
- "@wordpress/data": "^10.48.0",
57
- "@wordpress/deprecated": "^4.48.0",
58
- "@wordpress/element": "^8.0.0",
59
- "@wordpress/html-entities": "^4.48.0",
60
- "@wordpress/i18n": "^6.21.0",
61
- "@wordpress/is-shallow-equal": "^5.48.0",
62
- "@wordpress/private-apis": "^1.48.0",
63
- "@wordpress/rich-text": "^7.48.0",
64
- "@wordpress/sync": "^1.48.0",
65
- "@wordpress/undo-manager": "^1.48.0",
66
- "@wordpress/url": "^4.48.0",
67
- "@wordpress/warning": "^3.48.0",
51
+ "@types/react": "^18.3.27",
52
+ "@wordpress/api-fetch": "^7.48.1",
53
+ "@wordpress/block-editor": "^15.21.1",
54
+ "@wordpress/blocks": "^15.21.1",
55
+ "@wordpress/compose": "^8.1.1",
56
+ "@wordpress/data": "^10.48.1",
57
+ "@wordpress/deprecated": "^4.48.1",
58
+ "@wordpress/element": "^8.0.1",
59
+ "@wordpress/html-entities": "^4.48.1",
60
+ "@wordpress/i18n": "^6.21.1",
61
+ "@wordpress/is-shallow-equal": "^5.48.1",
62
+ "@wordpress/private-apis": "^1.48.1",
63
+ "@wordpress/rich-text": "^7.48.1",
64
+ "@wordpress/sync": "^1.48.1",
65
+ "@wordpress/undo-manager": "^1.48.1",
66
+ "@wordpress/url": "^4.48.1",
67
+ "@wordpress/warning": "^3.48.1",
68
68
  "change-case": "^4.1.2",
69
69
  "equivalent-key-map": "^0.2.2",
70
70
  "fast-deep-equal": "^3.1.3",
@@ -73,9 +73,11 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@jest/globals": "^30.2.0",
76
+ "@testing-library/dom": "^10.4.1",
77
+ "@testing-library/react": "^16.3.2",
76
78
  "@types/jest": "^29.5.14",
77
79
  "@types/node": "^20.19.39",
78
- "deep-freeze": "0.0.1"
80
+ "deep-freeze": "^0.0.1"
79
81
  },
80
82
  "peerDependencies": {
81
83
  "react": "^18.0.0",
@@ -84,5 +86,5 @@
84
86
  "publishConfig": {
85
87
  "access": "public"
86
88
  },
87
- "gitHead": "e7856693aeb4e2522d13608cd32c994e4a97cb9c"
89
+ "gitHead": "99df7432c5c7cb83ba41146fd1f57f3c19004305"
88
90
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { select } from '@wordpress/data';
4
+ import { useSelect } from '@wordpress/data';
5
5
  import { Y } from '@wordpress/sync';
6
6
  // @ts-ignore No exported types for block editor store selectors.
7
7
  import { store as blockEditorStore } from '@wordpress/block-editor';
@@ -10,17 +10,17 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
10
10
  * Internal dependencies
11
11
  */
12
12
  import type { AbsoluteBlockIndexPath } from '../types';
13
+ import { unlock } from '../lock-unlock';
13
14
 
14
15
  /**
15
- * A block as represented in the block-editor store (from `getBlocks()`).
16
+ * A block as represented in the block-editor store's client ID tree.
16
17
  *
17
18
  * This is a minimal interface covering only the fields used by RTC awareness.
18
19
  */
19
- interface EditorStoreBlock {
20
+ export type EditorStoreBlock = {
20
21
  clientId: string;
21
- name: string;
22
22
  innerBlocks: EditorStoreBlock[];
23
- }
23
+ };
24
24
 
25
25
  /**
26
26
  * Find the block Y.Map that contains a nested Yjs type.
@@ -112,26 +112,18 @@ export function getBlockPathInYdoc(
112
112
  * Navigate the block-editor store's block tree by an index path
113
113
  * and return the local block's clientId.
114
114
  *
115
- * In template mode, getBlocks() returns the full template tree, but Yjs
116
- * paths are relative to the post content. This method finds the
117
- * core/post-content block (if present) and uses its inner blocks as the
118
- * navigation root, so paths align with the Yjs document structure.
119
- *
120
- * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1].
115
+ * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1].
116
+ * @param blocks - The tree of block-editor store post contentblocks.
121
117
  * @return The local block clientId, or null if the path is invalid.
122
118
  */
123
119
  export function resolveBlockClientIdByPath(
124
- path: AbsoluteBlockIndexPath
120
+ path: AbsoluteBlockIndexPath,
121
+ blocks: EditorStoreBlock[]
125
122
  ): string | null {
126
123
  if ( path.length === 0 ) {
127
124
  return null;
128
125
  }
129
126
 
130
- const { getBlocks } = select( blockEditorStore );
131
- const postContentBlocks = getPostContentBlocks( getBlocks(), getBlocks );
132
-
133
- let blocks = postContentBlocks;
134
-
135
127
  for ( let i = 0; i < path.length; i++ ) {
136
128
  const block = blocks[ path[ i ] ];
137
129
  if ( ! block ) {
@@ -151,53 +143,20 @@ export function resolveBlockClientIdByPath(
151
143
  * In template mode, the block tree contains template parts wrapping a
152
144
  * core/post-content block. The Yjs document only stores the post content
153
145
  * blocks, so we need to find the core/post-content block and use
154
- * getBlocks(clientId) to retrieve its inner blocks from the store.
146
+ * getClientIdsTree(clientId) to retrieve its inner blocks from the store.
155
147
  *
156
- * We must use getBlocks(clientId) rather than reading .innerBlocks from
157
- * the block object because useBlockSync() injects post content as
158
- * controlled inner blocks they exist in the store's block order map
159
- * but are not populated in the .innerBlocks property of the tree
160
- * returned by getBlocks().
148
+ * Uses the private getClientIdsTree selector which depends only on
149
+ * state.blocks.order, avoiding unnecessary re-renders when block
150
+ * attributes change (which would happen with getBlocks()).
161
151
  *
162
- * @param rootBlocks - The root-level blocks from getBlocks().
163
- * @param getBlocks - The getBlocks selector.
164
152
  * @return The blocks that correspond to the Yjs document root.
165
153
  */
166
- function getPostContentBlocks(
167
- rootBlocks: EditorStoreBlock[],
168
- getBlocks: ( rootClientId?: string ) => EditorStoreBlock[]
169
- ): EditorStoreBlock[] {
170
- const postContentBlock = findBlockByName( rootBlocks, 'core/post-content' );
171
- if ( postContentBlock ) {
172
- // Use getBlocks(clientId) to read controlled inner blocks from
173
- // the store, since postContentBlock.innerBlocks is empty.
174
- return getBlocks( postContentBlock.clientId );
175
- }
176
-
177
- return rootBlocks;
178
- }
179
-
180
- /**
181
- * Recursively search the block tree for a block with a given name.
182
- *
183
- * @param blocks - The blocks to search.
184
- * @param name - The block name to find.
185
- * @return The first matching block, or null if not found.
186
- */
187
- function findBlockByName(
188
- blocks: EditorStoreBlock[],
189
- name: string
190
- ): EditorStoreBlock | null {
191
- for ( const block of blocks ) {
192
- if ( block.name === name ) {
193
- return block;
194
- }
195
- if ( block.innerBlocks?.length ) {
196
- const found = findBlockByName( block.innerBlocks, name );
197
- if ( found ) {
198
- return found;
199
- }
200
- }
201
- }
202
- return null;
154
+ export function usePostContentBlocks(): EditorStoreBlock[] {
155
+ return useSelect( ( select ) => {
156
+ const { getBlocksByName, getClientIdsTree } = unlock(
157
+ select( blockEditorStore )
158
+ );
159
+ const [ postContentClientId ] = getBlocksByName( 'core/post-content' );
160
+ return getClientIdsTree( postContentClientId ?? '' );
161
+ }, [] );
203
162
  }
@@ -37,6 +37,7 @@ import type {
37
37
  WPBlockSelection,
38
38
  } from '../types';
39
39
  import type { YBlocks } from '../utils/crdt-blocks';
40
+ import type { EditorStoreBlock } from './block-lookup';
40
41
  import type {
41
42
  DebugCollaboratorData,
42
43
  EditorState,
@@ -245,10 +246,12 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
245
246
  * clientIds (e.g. in "Show Template" mode where blocks are cloned).
246
247
  *
247
248
  * @param selection - The selection state.
249
+ * @param blocks - The tree of block-editor store post content blocks.
248
250
  * @return The rich-text offset and block client ID, or nulls if not resolvable.
249
251
  */
250
252
  public convertSelectionStateToAbsolute(
251
- selection: SelectionState
253
+ selection: SelectionState,
254
+ blocks: EditorStoreBlock[]
252
255
  ): ResolvedSelection {
253
256
  if ( selection.type === SelectionType.None ) {
254
257
  return {
@@ -273,7 +276,7 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
273
276
  if ( block instanceof Y.Map ) {
274
277
  const path = getBlockPathInYdoc( block );
275
278
  localClientId = path
276
- ? resolveBlockClientIdByPath( path )
279
+ ? resolveBlockClientIdByPath( path, blocks )
277
280
  : null;
278
281
  }
279
282
  }
@@ -306,7 +309,9 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
306
309
 
307
310
  const yType = getContainingBlockYMap( absolutePosition.type );
308
311
  const path = yType ? getBlockPathInYdoc( yType ) : null;
309
- const localClientId = path ? resolveBlockClientIdByPath( path ) : null;
312
+ const localClientId = path
313
+ ? resolveBlockClientIdByPath( path, blocks )
314
+ : null;
310
315
 
311
316
  return {
312
317
  richTextOffset: htmlIndexToRichTextOffset(
@@ -2,7 +2,7 @@
2
2
  * External dependencies
3
3
  */
4
4
  import { Y } from '@wordpress/sync';
5
- import { select } from '@wordpress/data';
5
+ import { renderHook } from '@testing-library/react';
6
6
 
7
7
  /**
8
8
  * Internal dependencies
@@ -11,23 +11,45 @@ import {
11
11
  getBlockPathInYdoc,
12
12
  getContainingBlockYMap,
13
13
  resolveBlockClientIdByPath,
14
+ usePostContentBlocks,
14
15
  } from '../block-lookup';
15
16
 
16
- // Mock WordPress dependencies
17
+ import type { EditorStoreBlock } from '../block-lookup';
18
+
19
+ type MockBlock = EditorStoreBlock & {
20
+ name: string;
21
+ innerBlocks: MockBlock[];
22
+ };
23
+
24
+ let mockGetClientIdsTree: jest.Mock;
25
+
26
+ function mockFlattenBlocks( blocks: MockBlock[] ): MockBlock[] {
27
+ return blocks.flatMap( ( b ) => [
28
+ b,
29
+ ...mockFlattenBlocks( b.innerBlocks ),
30
+ ] );
31
+ }
32
+
33
+ jest.mock( '../../lock-unlock', () => ( {
34
+ unlock: ( obj: any ) => obj,
35
+ } ) );
36
+
17
37
  jest.mock( '@wordpress/data', () => ( {
18
- select: jest.fn(),
38
+ useSelect: ( selector: Function ) =>
39
+ selector( () => ( {
40
+ getClientIdsTree: ( ...args: any[] ) =>
41
+ mockGetClientIdsTree( ...args ),
42
+ getBlocksByName: ( blockName: string ) =>
43
+ mockFlattenBlocks( mockGetClientIdsTree( '' ) )
44
+ .filter( ( b ) => b.name === blockName )
45
+ .map( ( b ) => b.clientId ),
46
+ } ) ),
19
47
  } ) );
20
48
 
21
49
  jest.mock( '@wordpress/block-editor', () => ( {
22
50
  store: 'core/block-editor',
23
51
  } ) );
24
52
 
25
- type MockBlock = {
26
- clientId: string;
27
- name: string;
28
- innerBlocks: MockBlock[];
29
- };
30
-
31
53
  /**
32
54
  * Create a Y.Map block with a clientId and empty innerBlocks, matching the
33
55
  * shape used by the Yjs block tree.
@@ -127,40 +149,6 @@ function createNestedYDoc( {
127
149
  return { ydoc, rootBlocks, innerBlocksArray };
128
150
  }
129
151
 
130
- /**
131
- * Mock the block-editor store's `getBlocks` selector.
132
- *
133
- * When called without an argument (or undefined), returns `rootBlocks`.
134
- * When called with a clientId, looks up the block by clientId and returns
135
- * its innerBlocks. This mimics how `getBlocks( clientId )` works in the
136
- * real store for controlled inner blocks.
137
- * @param rootBlocks
138
- */
139
- function mockBlockEditorStore( rootBlocks: MockBlock[] ) {
140
- const allBlocks = new Map< string, MockBlock >();
141
-
142
- function indexBlocks( blocks: MockBlock[] ) {
143
- for ( const block of blocks ) {
144
- allBlocks.set( block.clientId, block );
145
- if ( block.innerBlocks?.length ) {
146
- indexBlocks( block.innerBlocks );
147
- }
148
- }
149
- }
150
- indexBlocks( rootBlocks );
151
-
152
- const getBlocks = jest.fn( ( rootClientId?: string ) => {
153
- if ( rootClientId === undefined ) {
154
- return rootBlocks;
155
- }
156
- const block = allBlocks.get( rootClientId );
157
- return block ? block.innerBlocks : [];
158
- } );
159
-
160
- ( select as jest.Mock ).mockReturnValue( { getBlocks } );
161
- return { getBlocks };
162
- }
163
-
164
152
  describe( 'getBlockPathInYdoc', () => {
165
153
  it( 'should return path [0] for the first root block', () => {
166
154
  const { blocks } = createFlatYDoc( 3 );
@@ -313,35 +301,30 @@ describe( 'getContainingBlockYMap', () => {
313
301
  } );
314
302
 
315
303
  describe( 'resolveBlockClientIdByPath', () => {
316
- afterEach( () => {
317
- jest.restoreAllMocks();
318
- } );
319
-
320
304
  it( 'should return null for an empty path', () => {
321
- mockBlockEditorStore( [] );
322
- expect( resolveBlockClientIdByPath( [] ) ).toBeNull();
305
+ expect( resolveBlockClientIdByPath( [], [] ) ).toBeNull();
323
306
  } );
324
307
 
325
308
  it( 'should resolve a root block by single-element path', () => {
326
- mockBlockEditorStore( [
309
+ const blocks: MockBlock[] = [
327
310
  { clientId: 'a', name: 'core/paragraph', innerBlocks: [] },
328
311
  { clientId: 'b', name: 'core/heading', innerBlocks: [] },
329
- ] );
312
+ ];
330
313
 
331
- expect( resolveBlockClientIdByPath( [ 0 ] ) ).toBe( 'a' );
332
- expect( resolveBlockClientIdByPath( [ 1 ] ) ).toBe( 'b' );
314
+ expect( resolveBlockClientIdByPath( [ 0 ], blocks ) ).toBe( 'a' );
315
+ expect( resolveBlockClientIdByPath( [ 1 ], blocks ) ).toBe( 'b' );
333
316
  } );
334
317
 
335
318
  it( 'should return null for an out-of-bounds index', () => {
336
- mockBlockEditorStore( [
319
+ const blocks: MockBlock[] = [
337
320
  { clientId: 'a', name: 'core/paragraph', innerBlocks: [] },
338
- ] );
321
+ ];
339
322
 
340
- expect( resolveBlockClientIdByPath( [ 5 ] ) ).toBeNull();
323
+ expect( resolveBlockClientIdByPath( [ 5 ], blocks ) ).toBeNull();
341
324
  } );
342
325
 
343
326
  it( 'should resolve a nested inner block', () => {
344
- mockBlockEditorStore( [
327
+ const blocks: MockBlock[] = [
345
328
  {
346
329
  clientId: 'parent',
347
330
  name: 'core/group',
@@ -358,13 +341,15 @@ describe( 'resolveBlockClientIdByPath', () => {
358
341
  },
359
342
  ],
360
343
  },
361
- ] );
344
+ ];
362
345
 
363
- expect( resolveBlockClientIdByPath( [ 0, 1 ] ) ).toBe( 'child-1' );
346
+ expect( resolveBlockClientIdByPath( [ 0, 1 ], blocks ) ).toBe(
347
+ 'child-1'
348
+ );
364
349
  } );
365
350
 
366
351
  it( 'should return null when inner path index is out of bounds', () => {
367
- mockBlockEditorStore( [
352
+ const blocks: MockBlock[] = [
368
353
  {
369
354
  clientId: 'parent',
370
355
  name: 'core/group',
@@ -376,12 +361,12 @@ describe( 'resolveBlockClientIdByPath', () => {
376
361
  },
377
362
  ],
378
363
  },
379
- ] );
364
+ ];
380
365
 
381
- expect( resolveBlockClientIdByPath( [ 0, 5 ] ) ).toBeNull();
366
+ expect( resolveBlockClientIdByPath( [ 0, 5 ], blocks ) ).toBeNull();
382
367
  } );
383
368
 
384
- describe( 'template mode (getPostContentBlocks behavior)', () => {
369
+ describe( 'template mode (usePostContentBlocks behavior)', () => {
385
370
  it( 'should navigate through core/post-content in template mode', () => {
386
371
  const postContentInnerBlocks: MockBlock[] = [
387
372
  {
@@ -398,7 +383,7 @@ describe( 'resolveBlockClientIdByPath', () => {
398
383
 
399
384
  // Template structure: header → post-content → footer.
400
385
  // post-content's innerBlocks are empty in the tree (controlled
401
- // inner blocks), so getBlocks( postContentClientId ) is used.
386
+ // inner blocks), so getClientIdsTree( postContentClientId ) is used.
402
387
  const templateBlocks: MockBlock[] = [
403
388
  {
404
389
  clientId: 'header',
@@ -417,13 +402,11 @@ describe( 'resolveBlockClientIdByPath', () => {
417
402
  },
418
403
  ];
419
404
 
420
- const { getBlocks } = mockBlockEditorStore( templateBlocks );
421
-
422
- // Override getBlocks to return post content blocks when called
423
- // with the post-content clientId (mimicking controlled inner
424
- // blocks behavior in useBlockSync).
425
- getBlocks.mockImplementation( ( rootClientId?: string ) => {
426
- if ( rootClientId === undefined ) {
405
+ // Override getClientIdsTree to return post content blocks when
406
+ // called with the post-content clientId (mimicking controlled
407
+ // inner blocks behavior in useBlockSync).
408
+ mockGetClientIdsTree = jest.fn( ( rootClientId: string = '' ) => {
409
+ if ( rootClientId === '' ) {
427
410
  return templateBlocks;
428
411
  }
429
412
  if ( rootClientId === 'post-content' ) {
@@ -432,13 +415,18 @@ describe( 'resolveBlockClientIdByPath', () => {
432
415
  return [];
433
416
  } );
434
417
 
435
- // Path [0] should resolve to the first post content block,
436
- // not the first template block.
437
- expect( resolveBlockClientIdByPath( [ 0 ] ) ).toBe( 'post-para-0' );
438
- expect( resolveBlockClientIdByPath( [ 1 ] ) ).toBe( 'post-para-1' );
418
+ // The returned blocks should be post content blocks, not the template blocks.
419
+ const blocks = renderHook( () => usePostContentBlocks() ).result
420
+ .current;
421
+ expect( resolveBlockClientIdByPath( [ 0 ], blocks ) ).toBe(
422
+ 'post-para-0'
423
+ );
424
+ expect( resolveBlockClientIdByPath( [ 1 ], blocks ) ).toBe(
425
+ 'post-para-1'
426
+ );
439
427
  } );
440
428
 
441
- it( 'should call getBlocks with post-content clientId', () => {
429
+ it( 'should call getClientIdsTree with post-content clientId', () => {
442
430
  const templateBlocks: MockBlock[] = [
443
431
  {
444
432
  clientId: 'header',
@@ -452,9 +440,8 @@ describe( 'resolveBlockClientIdByPath', () => {
452
440
  },
453
441
  ];
454
442
 
455
- const { getBlocks } = mockBlockEditorStore( templateBlocks );
456
- getBlocks.mockImplementation( ( rootClientId?: string ) => {
457
- if ( rootClientId === undefined ) {
443
+ mockGetClientIdsTree = jest.fn( ( rootClientId: string = '' ) => {
444
+ if ( rootClientId === '' ) {
458
445
  return templateBlocks;
459
446
  }
460
447
  if ( rootClientId === 'pc' ) {
@@ -469,10 +456,10 @@ describe( 'resolveBlockClientIdByPath', () => {
469
456
  return [];
470
457
  } );
471
458
 
472
- resolveBlockClientIdByPath( [ 0 ] );
459
+ renderHook( () => usePostContentBlocks() );
473
460
 
474
- // Verify getBlocks was called with the post-content clientId.
475
- expect( getBlocks ).toHaveBeenCalledWith( 'pc' );
461
+ // Verify getClientIdsTree was called with the post-content clientId.
462
+ expect( mockGetClientIdsTree ).toHaveBeenCalledWith( 'pc' );
476
463
  } );
477
464
 
478
465
  it( 'should find core/post-content nested inside template parts', () => {
@@ -500,9 +487,8 @@ describe( 'resolveBlockClientIdByPath', () => {
500
487
  },
501
488
  ];
502
489
 
503
- const { getBlocks } = mockBlockEditorStore( templateBlocks );
504
- getBlocks.mockImplementation( ( rootClientId?: string ) => {
505
- if ( rootClientId === undefined ) {
490
+ mockGetClientIdsTree = jest.fn( ( rootClientId: string = '' ) => {
491
+ if ( rootClientId === '' ) {
506
492
  return templateBlocks;
507
493
  }
508
494
  if ( rootClientId === 'nested-pc' ) {
@@ -511,12 +497,16 @@ describe( 'resolveBlockClientIdByPath', () => {
511
497
  return [];
512
498
  } );
513
499
 
514
- expect( resolveBlockClientIdByPath( [ 0 ] ) ).toBe( 'deep-para' );
500
+ const blocks = renderHook( () => usePostContentBlocks() ).result
501
+ .current;
502
+ expect( resolveBlockClientIdByPath( [ 0 ], blocks ) ).toBe(
503
+ 'deep-para'
504
+ );
515
505
  } );
516
506
 
517
507
  it( 'should use root blocks directly when no core/post-content exists', () => {
518
508
  // No template mode — plain post editing.
519
- const blocks: MockBlock[] = [
509
+ const postContentBlocks: MockBlock[] = [
520
510
  {
521
511
  clientId: 'para-0',
522
512
  name: 'core/paragraph',
@@ -529,10 +519,22 @@ describe( 'resolveBlockClientIdByPath', () => {
529
519
  },
530
520
  ];
531
521
 
532
- mockBlockEditorStore( blocks );
522
+ mockGetClientIdsTree = jest.fn( ( rootClientId: string = '' ) => {
523
+ if ( rootClientId === '' ) {
524
+ return postContentBlocks;
525
+ }
526
+ return [];
527
+ } );
533
528
 
534
- expect( resolveBlockClientIdByPath( [ 0 ] ) ).toBe( 'para-0' );
535
- expect( resolveBlockClientIdByPath( [ 1 ] ) ).toBe( 'para-1' );
529
+ const blocks = renderHook( () => usePostContentBlocks() ).result
530
+ .current;
531
+
532
+ expect( resolveBlockClientIdByPath( [ 0 ], blocks ) ).toBe(
533
+ 'para-0'
534
+ );
535
+ expect( resolveBlockClientIdByPath( [ 1 ], blocks ) ).toBe(
536
+ 'para-1'
537
+ );
536
538
  } );
537
539
 
538
540
  it( 'should return null for invalid path in template mode', () => {
@@ -549,9 +551,8 @@ describe( 'resolveBlockClientIdByPath', () => {
549
551
  },
550
552
  ];
551
553
 
552
- const { getBlocks } = mockBlockEditorStore( templateBlocks );
553
- getBlocks.mockImplementation( ( rootClientId?: string ) => {
554
- if ( rootClientId === undefined ) {
554
+ mockGetClientIdsTree = jest.fn( ( rootClientId: string = '' ) => {
555
+ if ( rootClientId === '' ) {
555
556
  return templateBlocks;
556
557
  }
557
558
  if ( rootClientId === 'pc' ) {
@@ -567,8 +568,11 @@ describe( 'resolveBlockClientIdByPath', () => {
567
568
  return [];
568
569
  } );
569
570
 
571
+ const blocks = renderHook( () => usePostContentBlocks() ).result
572
+ .current;
573
+
570
574
  // Index 1 is out of bounds for the post content blocks.
571
- expect( resolveBlockClientIdByPath( [ 1 ] ) ).toBeNull();
575
+ expect( resolveBlockClientIdByPath( [ 1 ], blocks ) ).toBeNull();
572
576
  } );
573
577
  } );
574
578
  } );