@wordpress/core-data 7.39.1-next.v.202602091733.0 → 7.40.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.
- package/CHANGELOG.md +2 -0
- package/README.md +41 -0
- package/build/actions.cjs +52 -0
- package/build/actions.cjs.map +2 -2
- package/build/awareness/base-awareness.cjs +1 -8
- package/build/awareness/base-awareness.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/awareness/utils.cjs +8 -51
- package/build/awareness/utils.cjs.map +2 -2
- package/build/entities.cjs +7 -1
- package/build/entities.cjs.map +2 -2
- package/build/hooks/use-entity-block-editor.cjs +13 -19
- package/build/hooks/use-entity-block-editor.cjs.map +2 -2
- package/build/index.cjs +6 -1
- package/build/index.cjs.map +2 -2
- package/build/private-actions.cjs +8 -0
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +2 -1
- package/build/private-apis.cjs.map +2 -2
- package/build/private-selectors.cjs +5 -0
- package/build/private-selectors.cjs.map +2 -2
- package/build/reducer.cjs +31 -1
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +26 -1
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +15 -0
- package/build/selectors.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +5 -3
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt.cjs +23 -19
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +50 -0
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/base-awareness.mjs +1 -8
- package/build-module/awareness/base-awareness.mjs.map +2 -2
- package/build-module/awareness/utils.mjs +8 -51
- package/build-module/awareness/utils.mjs.map +2 -2
- package/build-module/entities.mjs +7 -1
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/use-entity-block-editor.mjs +13 -19
- package/build-module/hooks/use-entity-block-editor.mjs.map +2 -2
- package/build-module/index.mjs +3 -0
- package/build-module/index.mjs.map +2 -2
- package/build-module/private-actions.mjs +7 -0
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +6 -2
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/private-selectors.mjs +8 -1
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +29 -1
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +25 -1
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +14 -0
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +3 -2
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +25 -20
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts +12 -0
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/base-awareness.d.ts.map +1 -1
- package/build-types/awareness/test/awareness-state.d.ts +2 -0
- package/build-types/awareness/test/awareness-state.d.ts.map +1 -0
- package/build-types/awareness/test/base-awareness.d.ts +2 -0
- package/build-types/awareness/test/base-awareness.d.ts.map +1 -0
- package/build-types/awareness/test/post-editor-awareness.d.ts +2 -0
- package/build-types/awareness/test/post-editor-awareness.d.ts.map +1 -0
- package/build-types/awareness/test/typed-awareness.d.ts +2 -0
- package/build-types/awareness/test/typed-awareness.d.ts.map +1 -0
- package/build-types/awareness/test/utils.d.ts +2 -0
- package/build-types/awareness/test/utils.d.ts.map +1 -0
- package/build-types/awareness/types.d.ts +0 -1
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/awareness/utils.d.ts +2 -3
- package/build-types/awareness/utils.d.ts.map +1 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/hooks/test/use-post-editor-awareness-state.d.ts +2 -0
- package/build-types/hooks/test/use-post-editor-awareness-state.d.ts.map +1 -0
- package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -1
- package/build-types/index.d.ts +5 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +8 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +8 -1
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts +23 -0
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +3 -0
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +17 -0
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +9 -0
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +6 -4
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/actions.js +78 -0
- package/src/awareness/base-awareness.ts +1 -14
- package/src/awareness/test/awareness-state.ts +417 -0
- package/src/awareness/test/base-awareness.ts +321 -0
- package/src/awareness/test/post-editor-awareness.ts +561 -0
- package/src/awareness/test/typed-awareness.ts +148 -0
- package/src/awareness/test/utils.ts +305 -0
- package/src/awareness/types.ts +0 -1
- package/src/awareness/utils.ts +8 -82
- package/src/entities.js +7 -1
- package/src/hooks/test/use-post-editor-awareness-state.ts +477 -0
- package/src/hooks/use-entity-block-editor.js +15 -21
- package/src/index.js +7 -0
- package/src/private-actions.js +14 -0
- package/src/private-apis.js +5 -1
- package/src/private-selectors.ts +16 -1
- package/src/reducer.js +45 -0
- package/src/resolvers.js +31 -2
- package/src/selectors.ts +41 -0
- package/src/test/actions.js +79 -0
- package/src/test/entity-provider.js +74 -0
- package/src/test/resolvers.js +2 -0
- package/src/test/store.js +30 -0
- package/src/utils/crdt-blocks.ts +2 -2
- package/src/utils/crdt.ts +44 -29
- package/src/utils/test/crdt.ts +212 -7
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { Y } from '@wordpress/sync';
|
|
5
|
+
import { dispatch, select, subscribe, resolveSelect } from '@wordpress/data';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies
|
|
9
|
+
*/
|
|
10
|
+
import { PostEditorAwareness } from '../post-editor-awareness';
|
|
11
|
+
import { SelectionType } from '../../utils/crdt-user-selections';
|
|
12
|
+
import type { SelectionNone, SelectionCursor } from '../../types';
|
|
13
|
+
import { CRDT_RECORD_MAP_KEY } from '../../sync';
|
|
14
|
+
import type { CollaboratorInfo } from '../types';
|
|
15
|
+
|
|
16
|
+
// Mock WordPress dependencies
|
|
17
|
+
jest.mock( '@wordpress/data', () => ( {
|
|
18
|
+
dispatch: jest.fn(),
|
|
19
|
+
select: jest.fn(),
|
|
20
|
+
subscribe: jest.fn(),
|
|
21
|
+
resolveSelect: jest.fn(),
|
|
22
|
+
} ) );
|
|
23
|
+
|
|
24
|
+
jest.mock( '@wordpress/block-editor', () => ( {
|
|
25
|
+
store: 'core/block-editor',
|
|
26
|
+
} ) );
|
|
27
|
+
|
|
28
|
+
// Mock window.navigator.userAgent
|
|
29
|
+
const mockUserAgent = ( userAgent: string ) => {
|
|
30
|
+
Object.defineProperty( window.navigator, 'userAgent', {
|
|
31
|
+
value: userAgent,
|
|
32
|
+
configurable: true,
|
|
33
|
+
} );
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const mockAvatarUrls = {
|
|
37
|
+
'24': 'https://example.com/avatar-24.png',
|
|
38
|
+
'48': 'https://example.com/avatar-48.png',
|
|
39
|
+
'96': 'https://example.com/avatar-96.png',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const createMockUser = () => ( {
|
|
43
|
+
id: 1,
|
|
44
|
+
name: 'Test User',
|
|
45
|
+
slug: 'test-user',
|
|
46
|
+
avatar_urls: mockAvatarUrls,
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Helper function to create a Y.Doc with blocks structure for testing
|
|
51
|
+
*/
|
|
52
|
+
function createTestDocWithBlocks() {
|
|
53
|
+
const ydoc = new Y.Doc();
|
|
54
|
+
const documentMap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
|
|
55
|
+
const blocks = new Y.Array();
|
|
56
|
+
documentMap.set( 'blocks', blocks );
|
|
57
|
+
|
|
58
|
+
// Create a block with content
|
|
59
|
+
const block = new Y.Map();
|
|
60
|
+
block.set( 'clientId', 'block-1' );
|
|
61
|
+
const attrs = new Y.Map();
|
|
62
|
+
attrs.set( 'content', new Y.Text( 'Hello world' ) );
|
|
63
|
+
block.set( 'attributes', attrs );
|
|
64
|
+
block.set( 'innerBlocks', new Y.Array() );
|
|
65
|
+
blocks.push( [ block ] );
|
|
66
|
+
|
|
67
|
+
return ydoc;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe( 'PostEditorAwareness', () => {
|
|
71
|
+
let doc: Y.Doc;
|
|
72
|
+
let subscribeCallback: ( () => void ) | null = null;
|
|
73
|
+
let mockEditEntityRecord: jest.Mock;
|
|
74
|
+
|
|
75
|
+
beforeEach( () => {
|
|
76
|
+
jest.useFakeTimers();
|
|
77
|
+
doc = createTestDocWithBlocks();
|
|
78
|
+
|
|
79
|
+
mockUserAgent(
|
|
80
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
jest.spyOn( Date, 'now' ).mockReturnValue( 1704067200000 );
|
|
84
|
+
|
|
85
|
+
// Mock select to return block editor selectors
|
|
86
|
+
( select as jest.Mock ).mockReturnValue( {
|
|
87
|
+
getSelectionStart: jest.fn().mockReturnValue( {} ),
|
|
88
|
+
getSelectionEnd: jest.fn().mockReturnValue( {} ),
|
|
89
|
+
getSelectedBlocksInitialCaretPosition: jest
|
|
90
|
+
.fn()
|
|
91
|
+
.mockReturnValue( null ),
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
// Mock subscribe to capture the callback
|
|
95
|
+
( subscribe as jest.Mock ).mockImplementation( ( callback ) => {
|
|
96
|
+
subscribeCallback = callback;
|
|
97
|
+
return jest.fn(); // unsubscribe
|
|
98
|
+
} );
|
|
99
|
+
|
|
100
|
+
// Mock dispatch
|
|
101
|
+
mockEditEntityRecord = jest.fn();
|
|
102
|
+
( dispatch as jest.Mock ).mockReturnValue( {
|
|
103
|
+
editEntityRecord: mockEditEntityRecord,
|
|
104
|
+
} );
|
|
105
|
+
|
|
106
|
+
// Mock resolveSelect for getCurrentUser
|
|
107
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
108
|
+
getCurrentUser: jest.fn().mockResolvedValue( createMockUser() ),
|
|
109
|
+
} );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
afterEach( () => {
|
|
113
|
+
jest.useRealTimers();
|
|
114
|
+
jest.restoreAllMocks();
|
|
115
|
+
subscribeCallback = null;
|
|
116
|
+
doc.destroy();
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
describe( 'construction', () => {
|
|
120
|
+
test( 'should create instance with Y.Doc and entity info', () => {
|
|
121
|
+
const awareness = new PostEditorAwareness(
|
|
122
|
+
doc,
|
|
123
|
+
'postType',
|
|
124
|
+
'post',
|
|
125
|
+
123
|
|
126
|
+
);
|
|
127
|
+
expect( awareness ).toBeInstanceOf( PostEditorAwareness );
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
test( 'should have correct clientID from doc', () => {
|
|
131
|
+
const awareness = new PostEditorAwareness(
|
|
132
|
+
doc,
|
|
133
|
+
'postType',
|
|
134
|
+
'post',
|
|
135
|
+
123
|
|
136
|
+
);
|
|
137
|
+
expect( awareness.clientID ).toBe( doc.clientID );
|
|
138
|
+
} );
|
|
139
|
+
} );
|
|
140
|
+
|
|
141
|
+
describe( 'setUp', () => {
|
|
142
|
+
test( 'should be idempotent', () => {
|
|
143
|
+
const awareness = new PostEditorAwareness(
|
|
144
|
+
doc,
|
|
145
|
+
'postType',
|
|
146
|
+
'post',
|
|
147
|
+
123
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
awareness.setUp();
|
|
151
|
+
awareness.setUp();
|
|
152
|
+
|
|
153
|
+
// Subscribe should only be called once
|
|
154
|
+
expect( subscribe ).toHaveBeenCalledTimes( 1 );
|
|
155
|
+
} );
|
|
156
|
+
|
|
157
|
+
test( 'should subscribe to selection changes', () => {
|
|
158
|
+
const awareness = new PostEditorAwareness(
|
|
159
|
+
doc,
|
|
160
|
+
'postType',
|
|
161
|
+
'post',
|
|
162
|
+
123
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
awareness.setUp();
|
|
166
|
+
|
|
167
|
+
expect( subscribe ).toHaveBeenCalled();
|
|
168
|
+
expect( subscribeCallback ).not.toBeNull();
|
|
169
|
+
} );
|
|
170
|
+
} );
|
|
171
|
+
|
|
172
|
+
describe( 'selection change handling', () => {
|
|
173
|
+
test( 'should not trigger update when selection has not changed', () => {
|
|
174
|
+
const awareness = new PostEditorAwareness(
|
|
175
|
+
doc,
|
|
176
|
+
'postType',
|
|
177
|
+
'post',
|
|
178
|
+
123
|
|
179
|
+
);
|
|
180
|
+
awareness.setUp();
|
|
181
|
+
|
|
182
|
+
// Trigger subscribe callback with same selection
|
|
183
|
+
subscribeCallback?.();
|
|
184
|
+
|
|
185
|
+
// Should not call editEntityRecord for unchanged selection
|
|
186
|
+
expect( mockEditEntityRecord ).not.toHaveBeenCalled();
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
test( 'should trigger update when selection changes', () => {
|
|
190
|
+
const mockGetSelectionStart = jest
|
|
191
|
+
.fn()
|
|
192
|
+
.mockReturnValueOnce( {} )
|
|
193
|
+
.mockReturnValueOnce( {
|
|
194
|
+
clientId: 'block-1',
|
|
195
|
+
attributeKey: 'content',
|
|
196
|
+
offset: 5,
|
|
197
|
+
} );
|
|
198
|
+
|
|
199
|
+
const mockGetSelectionEnd = jest
|
|
200
|
+
.fn()
|
|
201
|
+
.mockReturnValueOnce( {} )
|
|
202
|
+
.mockReturnValueOnce( {
|
|
203
|
+
clientId: 'block-1',
|
|
204
|
+
attributeKey: 'content',
|
|
205
|
+
offset: 5,
|
|
206
|
+
} );
|
|
207
|
+
|
|
208
|
+
( select as jest.Mock ).mockReturnValue( {
|
|
209
|
+
getSelectionStart: mockGetSelectionStart,
|
|
210
|
+
getSelectionEnd: mockGetSelectionEnd,
|
|
211
|
+
getSelectedBlocksInitialCaretPosition: jest
|
|
212
|
+
.fn()
|
|
213
|
+
.mockReturnValue( null ),
|
|
214
|
+
} );
|
|
215
|
+
|
|
216
|
+
const awareness = new PostEditorAwareness(
|
|
217
|
+
doc,
|
|
218
|
+
'postType',
|
|
219
|
+
'post',
|
|
220
|
+
123
|
|
221
|
+
);
|
|
222
|
+
awareness.setUp();
|
|
223
|
+
|
|
224
|
+
// Trigger subscribe callback with new selection
|
|
225
|
+
subscribeCallback?.();
|
|
226
|
+
|
|
227
|
+
// Should call editEntityRecord
|
|
228
|
+
expect( mockEditEntityRecord ).toHaveBeenCalledWith(
|
|
229
|
+
'postType',
|
|
230
|
+
'post',
|
|
231
|
+
123,
|
|
232
|
+
expect.objectContaining( {
|
|
233
|
+
selection: expect.any( Object ),
|
|
234
|
+
} ),
|
|
235
|
+
expect.objectContaining( {
|
|
236
|
+
undoIgnore: true,
|
|
237
|
+
} )
|
|
238
|
+
);
|
|
239
|
+
} );
|
|
240
|
+
|
|
241
|
+
test( 'should debounce local cursor updates', () => {
|
|
242
|
+
const mockGetSelectionStart = jest
|
|
243
|
+
.fn()
|
|
244
|
+
.mockReturnValueOnce( {} )
|
|
245
|
+
.mockReturnValueOnce( {
|
|
246
|
+
clientId: 'block-1',
|
|
247
|
+
attributeKey: 'content',
|
|
248
|
+
offset: 5,
|
|
249
|
+
} );
|
|
250
|
+
|
|
251
|
+
const mockGetSelectionEnd = jest
|
|
252
|
+
.fn()
|
|
253
|
+
.mockReturnValueOnce( {} )
|
|
254
|
+
.mockReturnValueOnce( {
|
|
255
|
+
clientId: 'block-1',
|
|
256
|
+
attributeKey: 'content',
|
|
257
|
+
offset: 5,
|
|
258
|
+
} );
|
|
259
|
+
|
|
260
|
+
( select as jest.Mock ).mockReturnValue( {
|
|
261
|
+
getSelectionStart: mockGetSelectionStart,
|
|
262
|
+
getSelectionEnd: mockGetSelectionEnd,
|
|
263
|
+
getSelectedBlocksInitialCaretPosition: jest
|
|
264
|
+
.fn()
|
|
265
|
+
.mockReturnValue( null ),
|
|
266
|
+
} );
|
|
267
|
+
|
|
268
|
+
const awareness = new PostEditorAwareness(
|
|
269
|
+
doc,
|
|
270
|
+
'postType',
|
|
271
|
+
'post',
|
|
272
|
+
123
|
|
273
|
+
);
|
|
274
|
+
awareness.setUp();
|
|
275
|
+
|
|
276
|
+
// Trigger selection change
|
|
277
|
+
subscribeCallback?.();
|
|
278
|
+
|
|
279
|
+
// Advance timers past debounce
|
|
280
|
+
jest.advanceTimersByTime( 10 );
|
|
281
|
+
|
|
282
|
+
// Should have processed the debounced update
|
|
283
|
+
expect( mockEditEntityRecord ).toHaveBeenCalled();
|
|
284
|
+
} );
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
describe( 'areEditorStatesEqual', () => {
|
|
288
|
+
test( 'should return true when both states are undefined', () => {
|
|
289
|
+
const awareness = new PostEditorAwareness(
|
|
290
|
+
doc,
|
|
291
|
+
'postType',
|
|
292
|
+
'post',
|
|
293
|
+
123
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Access the protected method via testing
|
|
297
|
+
// We can test this indirectly through setLocalStateField behavior
|
|
298
|
+
awareness.setUp();
|
|
299
|
+
|
|
300
|
+
// Set editorState with a selection
|
|
301
|
+
const selectionState: SelectionNone = {
|
|
302
|
+
type: SelectionType.None,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
awareness.setLocalStateField( 'editorState', {
|
|
306
|
+
selection: selectionState,
|
|
307
|
+
} );
|
|
308
|
+
|
|
309
|
+
// Subscribe to track updates
|
|
310
|
+
const callback = jest.fn();
|
|
311
|
+
awareness.onStateChange( callback );
|
|
312
|
+
|
|
313
|
+
// Emit change event
|
|
314
|
+
awareness.emit( 'change', [
|
|
315
|
+
{
|
|
316
|
+
added: [],
|
|
317
|
+
updated: [ awareness.clientID ],
|
|
318
|
+
removed: [],
|
|
319
|
+
},
|
|
320
|
+
] );
|
|
321
|
+
callback.mockClear();
|
|
322
|
+
|
|
323
|
+
// Set same state again - should not trigger unnecessary updates
|
|
324
|
+
awareness.setLocalStateField( 'editorState', {
|
|
325
|
+
selection: selectionState,
|
|
326
|
+
} );
|
|
327
|
+
|
|
328
|
+
// Emit change event again
|
|
329
|
+
awareness.emit( 'change', [
|
|
330
|
+
{
|
|
331
|
+
added: [],
|
|
332
|
+
updated: [ awareness.clientID ],
|
|
333
|
+
removed: [],
|
|
334
|
+
},
|
|
335
|
+
] );
|
|
336
|
+
|
|
337
|
+
// Callback should not be called for equal editor states
|
|
338
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
339
|
+
} );
|
|
340
|
+
} );
|
|
341
|
+
|
|
342
|
+
describe( 'getAbsolutePositionIndex', () => {
|
|
343
|
+
test( 'should return null when relative position cannot be resolved', () => {
|
|
344
|
+
const awareness = new PostEditorAwareness(
|
|
345
|
+
doc,
|
|
346
|
+
'postType',
|
|
347
|
+
'post',
|
|
348
|
+
123
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Create a Y.Doc for creating a relative position, then destroy it
|
|
352
|
+
// This creates a relative position that cannot be resolved in the awareness doc
|
|
353
|
+
const tempDoc = new Y.Doc();
|
|
354
|
+
const tempText = tempDoc.getText( 'temp' );
|
|
355
|
+
tempText.insert( 0, 'Hello' );
|
|
356
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
357
|
+
tempText,
|
|
358
|
+
2
|
|
359
|
+
);
|
|
360
|
+
tempDoc.destroy();
|
|
361
|
+
|
|
362
|
+
const selection: SelectionCursor = {
|
|
363
|
+
type: SelectionType.Cursor,
|
|
364
|
+
blockId: 'block-1',
|
|
365
|
+
cursorPosition: {
|
|
366
|
+
relativePosition,
|
|
367
|
+
absoluteOffset: 2,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const result = awareness.getAbsolutePositionIndex( selection );
|
|
372
|
+
|
|
373
|
+
// Should return null when the relative position's type cannot be found
|
|
374
|
+
expect( result ).toBeNull();
|
|
375
|
+
} );
|
|
376
|
+
|
|
377
|
+
test( 'should return absolute position index for valid selection', () => {
|
|
378
|
+
const awareness = new PostEditorAwareness(
|
|
379
|
+
doc,
|
|
380
|
+
'postType',
|
|
381
|
+
'post',
|
|
382
|
+
123
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Get the Y.Text from the doc
|
|
386
|
+
const documentMap = doc.getMap( CRDT_RECORD_MAP_KEY );
|
|
387
|
+
const blocks = documentMap.get( 'blocks' ) as Y.Array<
|
|
388
|
+
Y.Map< any >
|
|
389
|
+
>;
|
|
390
|
+
const block = blocks.get( 0 );
|
|
391
|
+
const attrs = block.get( 'attributes' ) as Y.Map< Y.Text >;
|
|
392
|
+
const yText = attrs.get( 'content' );
|
|
393
|
+
|
|
394
|
+
// Create a relative position
|
|
395
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
396
|
+
yText as Y.Text,
|
|
397
|
+
5
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const selection: SelectionCursor = {
|
|
401
|
+
type: SelectionType.Cursor,
|
|
402
|
+
blockId: 'block-1',
|
|
403
|
+
cursorPosition: {
|
|
404
|
+
relativePosition,
|
|
405
|
+
absoluteOffset: 5,
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const result = awareness.getAbsolutePositionIndex( selection );
|
|
410
|
+
|
|
411
|
+
expect( result ).toBe( 5 );
|
|
412
|
+
} );
|
|
413
|
+
} );
|
|
414
|
+
|
|
415
|
+
describe( 'getDebugData', () => {
|
|
416
|
+
test( 'should return debug data object', async () => {
|
|
417
|
+
const awareness = new PostEditorAwareness(
|
|
418
|
+
doc,
|
|
419
|
+
'postType',
|
|
420
|
+
'post',
|
|
421
|
+
123
|
|
422
|
+
);
|
|
423
|
+
awareness.setUp();
|
|
424
|
+
|
|
425
|
+
// Wait for async setup
|
|
426
|
+
await Promise.resolve();
|
|
427
|
+
|
|
428
|
+
// Emit a change to populate seenStates
|
|
429
|
+
awareness.emit( 'change', [
|
|
430
|
+
{
|
|
431
|
+
added: [],
|
|
432
|
+
updated: [ awareness.clientID ],
|
|
433
|
+
removed: [],
|
|
434
|
+
},
|
|
435
|
+
] );
|
|
436
|
+
|
|
437
|
+
const debugData = awareness.getDebugData();
|
|
438
|
+
|
|
439
|
+
expect( debugData ).toHaveProperty( 'doc' );
|
|
440
|
+
expect( debugData ).toHaveProperty( 'clients' );
|
|
441
|
+
expect( debugData ).toHaveProperty( 'collaboratorMap' );
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
test( 'should include document data', async () => {
|
|
445
|
+
const awareness = new PostEditorAwareness(
|
|
446
|
+
doc,
|
|
447
|
+
'postType',
|
|
448
|
+
'post',
|
|
449
|
+
123
|
|
450
|
+
);
|
|
451
|
+
awareness.setUp();
|
|
452
|
+
await Promise.resolve();
|
|
453
|
+
|
|
454
|
+
const debugData = awareness.getDebugData();
|
|
455
|
+
|
|
456
|
+
expect( debugData.doc ).toHaveProperty( CRDT_RECORD_MAP_KEY );
|
|
457
|
+
} );
|
|
458
|
+
|
|
459
|
+
test( 'should include client items', async () => {
|
|
460
|
+
const awareness = new PostEditorAwareness(
|
|
461
|
+
doc,
|
|
462
|
+
'postType',
|
|
463
|
+
'post',
|
|
464
|
+
123
|
|
465
|
+
);
|
|
466
|
+
awareness.setUp();
|
|
467
|
+
await Promise.resolve();
|
|
468
|
+
|
|
469
|
+
const debugData = awareness.getDebugData();
|
|
470
|
+
|
|
471
|
+
expect( debugData.clients ).toBeDefined();
|
|
472
|
+
expect( typeof debugData.clients ).toBe( 'object' );
|
|
473
|
+
} );
|
|
474
|
+
} );
|
|
475
|
+
|
|
476
|
+
describe( 'equalityFieldChecks', () => {
|
|
477
|
+
test( 'should include collaboratorInfo check from baseEqualityFieldChecks', async () => {
|
|
478
|
+
const awareness = new PostEditorAwareness(
|
|
479
|
+
doc,
|
|
480
|
+
'postType',
|
|
481
|
+
'post',
|
|
482
|
+
123
|
|
483
|
+
);
|
|
484
|
+
awareness.setUp();
|
|
485
|
+
await Promise.resolve();
|
|
486
|
+
|
|
487
|
+
// Set collaboratorInfo and verify equality check works
|
|
488
|
+
const collaboratorInfo: CollaboratorInfo = {
|
|
489
|
+
id: 1,
|
|
490
|
+
name: 'Test',
|
|
491
|
+
slug: 'test',
|
|
492
|
+
avatar_urls: mockAvatarUrls,
|
|
493
|
+
browserType: 'Chrome',
|
|
494
|
+
enteredAt: 1704067200000,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
awareness.setLocalStateField(
|
|
498
|
+
'collaboratorInfo',
|
|
499
|
+
collaboratorInfo
|
|
500
|
+
);
|
|
501
|
+
const storedInfo =
|
|
502
|
+
awareness.getLocalStateField( 'collaboratorInfo' );
|
|
503
|
+
|
|
504
|
+
expect( storedInfo ).toEqual( collaboratorInfo );
|
|
505
|
+
} );
|
|
506
|
+
|
|
507
|
+
test( 'should include editorState check', async () => {
|
|
508
|
+
const awareness = new PostEditorAwareness(
|
|
509
|
+
doc,
|
|
510
|
+
'postType',
|
|
511
|
+
'post',
|
|
512
|
+
123
|
|
513
|
+
);
|
|
514
|
+
awareness.setUp();
|
|
515
|
+
await Promise.resolve();
|
|
516
|
+
|
|
517
|
+
const editorState = {
|
|
518
|
+
selection: {
|
|
519
|
+
type: SelectionType.None,
|
|
520
|
+
} as SelectionNone,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
awareness.setLocalStateField( 'editorState', editorState );
|
|
524
|
+
const storedState = awareness.getLocalStateField( 'editorState' );
|
|
525
|
+
|
|
526
|
+
expect( storedState ).toEqual( editorState );
|
|
527
|
+
} );
|
|
528
|
+
} );
|
|
529
|
+
|
|
530
|
+
describe( 'state subscription', () => {
|
|
531
|
+
test( 'should notify subscribers on editorState change', async () => {
|
|
532
|
+
const awareness = new PostEditorAwareness(
|
|
533
|
+
doc,
|
|
534
|
+
'postType',
|
|
535
|
+
'post',
|
|
536
|
+
123
|
|
537
|
+
);
|
|
538
|
+
const callback = jest.fn();
|
|
539
|
+
|
|
540
|
+
awareness.onStateChange( callback );
|
|
541
|
+
awareness.setUp();
|
|
542
|
+
await Promise.resolve();
|
|
543
|
+
|
|
544
|
+
// Set initial state to trigger callback
|
|
545
|
+
awareness.setLocalStateField( 'editorState', {
|
|
546
|
+
selection: { type: SelectionType.None },
|
|
547
|
+
} );
|
|
548
|
+
|
|
549
|
+
// Emit change event
|
|
550
|
+
awareness.emit( 'change', [
|
|
551
|
+
{
|
|
552
|
+
added: [],
|
|
553
|
+
updated: [ awareness.clientID ],
|
|
554
|
+
removed: [],
|
|
555
|
+
},
|
|
556
|
+
] );
|
|
557
|
+
|
|
558
|
+
expect( callback ).toHaveBeenCalled();
|
|
559
|
+
} );
|
|
560
|
+
} );
|
|
561
|
+
} );
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WordPress dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { Y } from '@wordpress/sync';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies
|
|
13
|
+
*/
|
|
14
|
+
import { TypedAwareness } from '../typed-awareness';
|
|
15
|
+
import type { EnhancedState } from '../types';
|
|
16
|
+
|
|
17
|
+
interface TestState {
|
|
18
|
+
name: string;
|
|
19
|
+
count: number;
|
|
20
|
+
isActive: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe( 'TypedAwareness', () => {
|
|
24
|
+
let doc: Y.Doc;
|
|
25
|
+
let awareness: TypedAwareness< TestState >;
|
|
26
|
+
|
|
27
|
+
beforeEach( () => {
|
|
28
|
+
doc = new Y.Doc();
|
|
29
|
+
awareness = new TypedAwareness< TestState >( doc );
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
afterEach( () => {
|
|
33
|
+
doc.destroy();
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
describe( 'getStates', () => {
|
|
37
|
+
test( 'should return empty map initially', () => {
|
|
38
|
+
const states = awareness.getStates();
|
|
39
|
+
// There should be at least one state (the local client)
|
|
40
|
+
expect( states ).toBeInstanceOf( Map );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
test( 'should return map with correct type', () => {
|
|
44
|
+
awareness.setLocalStateField( 'name', 'Test' );
|
|
45
|
+
awareness.setLocalStateField( 'count', 42 );
|
|
46
|
+
|
|
47
|
+
const states = awareness.getStates();
|
|
48
|
+
const localState = states.get( awareness.clientID );
|
|
49
|
+
|
|
50
|
+
expect( localState?.name ).toBe( 'Test' );
|
|
51
|
+
expect( localState?.count ).toBe( 42 );
|
|
52
|
+
} );
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
describe( 'getLocalStateField', () => {
|
|
56
|
+
test( 'should return null for non-existent field', () => {
|
|
57
|
+
const result = awareness.getLocalStateField( 'name' );
|
|
58
|
+
expect( result ).toBeNull();
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
test( 'should return field value after setting', () => {
|
|
62
|
+
awareness.setLocalStateField( 'name', 'John' );
|
|
63
|
+
const result = awareness.getLocalStateField( 'name' );
|
|
64
|
+
expect( result ).toBe( 'John' );
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
test( 'should return correct type for number field', () => {
|
|
68
|
+
awareness.setLocalStateField( 'count', 100 );
|
|
69
|
+
const result = awareness.getLocalStateField( 'count' );
|
|
70
|
+
expect( result ).toBe( 100 );
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
test( 'should return correct type for boolean field', () => {
|
|
74
|
+
awareness.setLocalStateField( 'isActive', true );
|
|
75
|
+
const result = awareness.getLocalStateField( 'isActive' );
|
|
76
|
+
expect( result ).toBe( true );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
test( 'should return null when local state is null', () => {
|
|
80
|
+
// Clear local state
|
|
81
|
+
awareness.setLocalState( null );
|
|
82
|
+
const result = awareness.getLocalStateField( 'name' );
|
|
83
|
+
expect( result ).toBeNull();
|
|
84
|
+
} );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
describe( 'setLocalStateField', () => {
|
|
88
|
+
test( 'should set string field', () => {
|
|
89
|
+
awareness.setLocalStateField( 'name', 'Alice' );
|
|
90
|
+
expect( awareness.getLocalStateField( 'name' ) ).toBe( 'Alice' );
|
|
91
|
+
} );
|
|
92
|
+
|
|
93
|
+
test( 'should set number field', () => {
|
|
94
|
+
awareness.setLocalStateField( 'count', 42 );
|
|
95
|
+
expect( awareness.getLocalStateField( 'count' ) ).toBe( 42 );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
test( 'should set boolean field', () => {
|
|
99
|
+
awareness.setLocalStateField( 'isActive', false );
|
|
100
|
+
expect( awareness.getLocalStateField( 'isActive' ) ).toBe( false );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
test( 'should update existing field value', () => {
|
|
104
|
+
awareness.setLocalStateField( 'name', 'First' );
|
|
105
|
+
awareness.setLocalStateField( 'name', 'Second' );
|
|
106
|
+
expect( awareness.getLocalStateField( 'name' ) ).toBe( 'Second' );
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
test( 'should preserve other fields when setting one field', () => {
|
|
110
|
+
awareness.setLocalStateField( 'name', 'Test' );
|
|
111
|
+
awareness.setLocalStateField( 'count', 10 );
|
|
112
|
+
|
|
113
|
+
expect( awareness.getLocalStateField( 'name' ) ).toBe( 'Test' );
|
|
114
|
+
expect( awareness.getLocalStateField( 'count' ) ).toBe( 10 );
|
|
115
|
+
} );
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
describe( 'clientID', () => {
|
|
119
|
+
test( 'should have a numeric clientID', () => {
|
|
120
|
+
expect( typeof awareness.clientID ).toBe( 'number' );
|
|
121
|
+
} );
|
|
122
|
+
|
|
123
|
+
test( 'should have the same clientID as the doc', () => {
|
|
124
|
+
expect( awareness.clientID ).toBe( doc.clientID );
|
|
125
|
+
} );
|
|
126
|
+
} );
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
describe( 'EnhancedState type', () => {
|
|
130
|
+
test( 'should include base state properties and metadata', () => {
|
|
131
|
+
// This is a compile-time type check
|
|
132
|
+
const enhancedState: EnhancedState< TestState > = {
|
|
133
|
+
name: 'Test',
|
|
134
|
+
count: 5,
|
|
135
|
+
isActive: true,
|
|
136
|
+
clientId: 123,
|
|
137
|
+
isConnected: true,
|
|
138
|
+
isMe: false,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
expect( enhancedState.name ).toBe( 'Test' );
|
|
142
|
+
expect( enhancedState.count ).toBe( 5 );
|
|
143
|
+
expect( enhancedState.isActive ).toBe( true );
|
|
144
|
+
expect( enhancedState.clientId ).toBe( 123 );
|
|
145
|
+
expect( enhancedState.isConnected ).toBe( true );
|
|
146
|
+
expect( enhancedState.isMe ).toBe( false );
|
|
147
|
+
} );
|
|
148
|
+
} );
|