Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,18 @@ export const getEntityRecord =
query
);
},
// Save the current entity record's unsaved edits.
// Save the current entity record, whether or not it has unsaved
// edits. This is used to trigger a persisted CRDT document.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be worth adding here that this creates a persisted CRDT document if it doesn't exist. That way this particular issue is noted here.

saveRecord: () => {
dispatch.saveEditedEntityRecord( kind, name, key );
resolveSelect
.getEditedEntityRecord( kind, name, key )
.then( ( editedRecord ) => {
dispatch.saveEntityRecord(
kind,
name,
editedRecord
);
} );
},
addUndoMeta: ( ydoc, meta ) => {
const selectionHistory =
Expand Down
108 changes: 108 additions & 0 deletions packages/core-data/src/test/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,114 @@ describe( 'getEntityRecord', () => {
);
} );

it( 'saveRecord fetches edited record and saves full entity record', async () => {
const POST_RECORD = { id: 1, title: 'Test Post' };
const EDITED_RECORD = { id: 1, title: 'Edited Post' };
const POST_RESPONSE = {
json: () => Promise.resolve( POST_RECORD ),
};
const ENTITIES_WITH_SYNC = [
{
name: 'post',
kind: 'postType',
baseURL: '/wp/v2/posts',
baseURLParams: { context: 'edit' },
syncConfig: {},
},
];

dispatch.saveEntityRecord = jest.fn();

const resolveSelectWithSync = {
getEntitiesConfig: jest.fn( () => ENTITIES_WITH_SYNC ),
getEditedEntityRecord: jest.fn( () =>
Promise.resolve( EDITED_RECORD )
),
};

triggerFetch.mockImplementation( () => POST_RESPONSE );

await getEntityRecord(
'postType',
'post',
1
)( {
dispatch,
registry,
resolveSelect: resolveSelectWithSync,
} );

// Extract the handlers passed to syncManager.load.
const handlers = syncManager.load.mock.calls[ 0 ][ 4 ];

// Call saveRecord and wait for the internal promise chain.
handlers.saveRecord();
await resolveSelectWithSync.getEditedEntityRecord();

// Should have fetched the full edited entity record.
expect(
resolveSelectWithSync.getEditedEntityRecord
).toHaveBeenCalledWith( 'postType', 'post', 1 );

// Should have called saveEntityRecord (not saveEditedEntityRecord).
expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith(
'postType',
'post',
EDITED_RECORD
);
} );

it( 'saveRecord saves even when there are no unsaved edits', async () => {
const POST_RECORD = { id: 1, title: 'Test Post' };
const POST_RESPONSE = {
json: () => Promise.resolve( POST_RECORD ),
};
const ENTITIES_WITH_SYNC = [
{
name: 'post',
kind: 'postType',
baseURL: '/wp/v2/posts',
baseURLParams: { context: 'edit' },
syncConfig: {},
},
];

dispatch.saveEntityRecord = jest.fn();

// Return the same record (no edits) from getEditedEntityRecord.
const resolveSelectWithSync = {
getEntitiesConfig: jest.fn( () => ENTITIES_WITH_SYNC ),
getEditedEntityRecord: jest.fn( () =>
Promise.resolve( POST_RECORD )
),
};

triggerFetch.mockImplementation( () => POST_RESPONSE );

await getEntityRecord(
'postType',
'post',
1
)( {
dispatch,
registry,
resolveSelect: resolveSelectWithSync,
} );

const handlers = syncManager.load.mock.calls[ 0 ][ 4 ];

// Call saveRecord and wait for the internal promise chain.
handlers.saveRecord();
await resolveSelectWithSync.getEditedEntityRecord();

// Should save the record even with no edits (the whole point of the fix).
expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith(
'postType',
'post',
POST_RECORD
);
} );

it( 'provides transient properties when read/write config is supplied', async () => {
const POST_RECORD = { id: 1, title: 'Test Post' };
const POST_RESPONSE = {
Expand Down
2 changes: 1 addition & 1 deletion packages/sync/src/test/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe( 'SyncManager', () => {
onStatusChange: jest.fn(),
refetchRecord: jest.fn( async () => Promise.resolve() ),
restoreUndoMeta: jest.fn(),
saveRecord: jest.fn( async () => Promise.resolve() ),
saveRecord: jest.fn(),
};
} );

Expand Down
2 changes: 1 addition & 1 deletion packages/sync/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export interface RecordHandlers {
onStatusChange: OnStatusChangeCallback;
refetchRecord: () => Promise< void >;
restoreUndoMeta: ( ydoc: Y.Doc, meta: Map< string, any > ) => void;
saveRecord: () => Promise< void >;
saveRecord: () => void;
}

export interface SyncConfig {
Expand Down
119 changes: 119 additions & 0 deletions test/e2e/specs/editor/collaboration/collaboration-persistence.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Internal dependencies
*/
import { test, expect } from './fixtures';

test.describe( 'Collaboration - CRDT persistence', () => {
test( 'persists CRDT document when loading existing post without one', async ( {
admin,
collaborationUtils,
editor,
page,
requestUtils,
} ) => {
await collaborationUtils.setCollaboration( true );

// Create a draft post — it will not have _crdt_document meta.
const post = await requestUtils.createPost( {
title: 'Persistence Test - Draft',
status: 'draft',
date_gmt: new Date().toISOString(),
} );

// Open the post in the editor.
await admin.visitAdminPage(
'post.php',
`post=${ post.id }&action=edit`
);
await editor.setPreferences( 'core/edit-post', {
welcomeGuide: false,
fullscreenMode: false,
} );

// Wait for the entity record resolver to finish.
await page.waitForFunction(
() => {
const postId = ( window as any ).wp.data
.select( 'core/editor' )
.getCurrentPostId();
if ( ! postId ) {
return false;
}
return ( window as any ).wp.data
.select( 'core' )
.hasFinishedResolution( 'getEntityRecord', [
'postType',
'post',
postId,
] );
},
{ timeout: 5000 }
);

const persistedCrdtDoc = await page.evaluate( () => {
return window.wp.data
.select( 'core' )
.getEntityRecord(
'postType',
'post',
window.wp.data.select( 'core/editor' ).getCurrentPostId()
).meta._crdt_document;
} );

expect( persistedCrdtDoc ).toBeTruthy();
} );

test( 'does not save CRDT document for auto-draft posts', async ( {
admin,
collaborationUtils,
editor,
page,
} ) => {
await collaborationUtils.setCollaboration( true );

// Navigate to create a new post (auto-draft).
await admin.visitAdminPage( 'post-new.php' );
await editor.setPreferences( 'core/edit-post', {
welcomeGuide: false,
fullscreenMode: false,
} );

// Wait for collaboration runtime to initialize.
await page.waitForFunction(
() => ( window as any )._wpCollaborationEnabled === true,
{ timeout: 5000 }
);

// Wait for the entity record resolver to finish.
await page.waitForFunction(
() => {
const postId = ( window as any ).wp.data
.select( 'core/editor' )
.getCurrentPostId();
if ( ! postId ) {
return false;
}
return ( window as any ).wp.data
.select( 'core' )
.hasFinishedResolution( 'getEntityRecord', [
'postType',
'post',
postId,
] );
},
{ timeout: 5000 }
);

const persistedCrdtDoc = await page.evaluate( () => {
return window.wp.data
.select( 'core' )
.getEntityRecord(
'postType',
'post',
window.wp.data.select( 'core/editor' ).getCurrentPostId()
).meta._crdt_document;
} );

expect( persistedCrdtDoc ).toBeFalsy();
} );
} );
Loading