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
4 changes: 4 additions & 0 deletions packages/core-data/src/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export const setCollaborationSupported =
dispatch( { type: 'SET_COLLABORATION_SUPPORTED', supported } );
if ( ! supported && hasSyncManager() ) {
getSyncManager().unloadAll();
dispatch.__unstableNotifySyncUndoManagerChange( {
hasUndo: false,
hasRedo: false,
} );
}
};

Expand Down
7 changes: 6 additions & 1 deletion packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,13 @@ export const getEntityRecord =
} );
}

const syncManager =
select?.isCollaborationSupported?.() === false
? undefined
: getSyncManager();

// Load the entity record for syncing. Do not await promise.
void getSyncManager()?.load(
void syncManager?.load(
entityConfig.syncConfig,
objectType,
objectId,
Expand Down
39 changes: 38 additions & 1 deletion packages/core-data/src/test/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { editMediaEntity } from '../private-actions';
import { editMediaEntity, setCollaborationSupported } from '../private-actions';
import { getSyncManager, hasSyncManager } from '../sync';

jest.mock( '@wordpress/api-fetch' );
jest.mock( '../sync', () => ( {
getSyncManager: jest.fn(),
hasSyncManager: jest.fn(),
} ) );

describe( 'editMediaEntity', () => {
let dispatch;
Expand Down Expand Up @@ -211,3 +216,35 @@ describe( 'editMediaEntity', () => {
expect( result ).toBeUndefined();
} );
} );

describe( 'setCollaborationSupported', () => {
afterEach( () => {
getSyncManager.mockReset();
hasSyncManager.mockReset();
} );

it( 'unloads sync and resets sync undo state when disabling collaboration', () => {
const syncManager = {
unloadAll: jest.fn(),
};
const dispatch = Object.assign( jest.fn(), {
__unstableNotifySyncUndoManagerChange: jest.fn(),
} );
hasSyncManager.mockReturnValue( true );
getSyncManager.mockReturnValue( syncManager );

setCollaborationSupported( false )( { dispatch } );

expect( dispatch ).toHaveBeenCalledWith( {
type: 'SET_COLLABORATION_SUPPORTED',
supported: false,
} );
expect( syncManager.unloadAll ).toHaveBeenCalledTimes( 1 );
expect(
dispatch.__unstableNotifySyncUndoManagerChange
).toHaveBeenCalledWith( {
hasUndo: false,
hasRedo: false,
} );
} );
} );
45 changes: 45 additions & 0 deletions packages/core-data/src/test/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,51 @@ describe( 'getEntityRecord', () => {
);
} );

it( 'does not load entity with sync manager when collaboration is unsupported', 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: {},
},
];

const resolveSelectWithSync = {
getEntitiesConfig: jest.fn( () => ENTITIES_WITH_SYNC ),
};
const select = {
isCollaborationSupported: jest.fn( () => false ),
};

triggerFetch.mockImplementation( () => POST_RESPONSE );

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

expect( syncManager.load ).not.toHaveBeenCalled();
expect( select.isCollaborationSupported ).toHaveBeenCalledTimes( 1 );
expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith(
'postType',
'post',
POST_RECORD,
undefined
);
} );

it( 'notifies core-data when the sync undo manager stack changes', async () => {
const POST_RECORD = { id: 1, title: 'Test Post' };
const POST_RESPONSE = {
Expand Down
1 change: 1 addition & 0 deletions packages/sync/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ export function createSyncManager( debug = false ): SyncManager {
entityState.unload();
}
entityStates.clear();
undoManager = undefined;

for ( const [ , collectionState ] of [ ...collectionStates ] ) {
collectionState.unload();
Expand Down
25 changes: 25 additions & 0 deletions packages/sync/src/test/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,31 @@ describe( 'SyncManager', () => {
expect( mockSyncConfig.applyChangesToCRDTDoc ).toHaveBeenCalled();
} );

it( 'clears the undo manager after unloading all entities', async () => {
const manager = createSyncManager();

await manager.load(
mockSyncConfig,
'post',
'123',
mockRecord,
mockHandlers
);
await manager.load(
mockSyncConfig,
'post',
'456',
mockRecord,
mockHandlers
);

expect( manager.undoManager ).toBeDefined();

manager.unloadAll();

expect( manager.undoManager ).toBeUndefined();
} );

it( 'destroys providers and skips initialization when unload runs during load', async () => {
// Hold provider creation open so we can interrupt the load between
// `entityStates.set(...)` and the provider creation resolving.
Expand Down
107 changes: 73 additions & 34 deletions test/e2e/specs/editor/collaboration/collaboration-metabox-lock.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
/**
* External dependencies
*/
import type { Page } from '@playwright/test';

/**
* Internal dependencies
*/
import { test, expect } from './fixtures';
import { SECOND_USER } from './fixtures/collaboration-utils';

const BASE_URL = process.env.WP_BASE_URL || 'http://localhost:8889';
const CORE_DATA_PRIVATE_APIS_CONSENT =
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.';

async function waitForCollaborationEnabledForCurrentPost(
page: Page,
expected: boolean
) {
await page.waitForFunction(
( { consent, expectedValue } ) => {
const privateApis = ( window as any ).wp.privateApis;
const { unlock } =
privateApis.__dangerousOptInToUnstableAPIsOnlyForCoreModules(
consent,
'@wordpress/core-data'
);
return (
unlock(
window.wp.data.select( 'core/editor' )
).isCollaborationEnabledForCurrentPost() === expectedValue
);
},
{
consent: CORE_DATA_PRIVATE_APIS_CONSENT,
expectedValue: expected,
},
{ timeout: 15000 }
);
}

test.describe( 'Collaboration with meta boxes', () => {
test.describe( 'incompatible meta boxes', () => {
Expand Down Expand Up @@ -42,23 +75,7 @@ test.describe( 'Collaboration with meta boxes', () => {
// Wait for meta boxes to initialize and disable collaboration.
// collaborationSupported starts as true, then the meta box hook
// sets it to false once meta boxes are detected.
await page.waitForFunction(
( consent ) => {
const privateApis = ( window as any ).wp.privateApis;
const { unlock } =
privateApis.__dangerousOptInToUnstableAPIsOnlyForCoreModules(
consent,
'@wordpress/core-data'
);
return (
unlock(
window.wp.data.select( 'core/editor' )
).isCollaborationEnabledForCurrentPost() === false
);
},
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
{ timeout: 15000 }
);
await waitForCollaborationEnabledForCurrentPost( page, false );

// Set up second browser context for User 2.
const secondContext = await admin.browser.newContext( {
Expand Down Expand Up @@ -114,6 +131,44 @@ test.describe( 'Collaboration with meta boxes', () => {
await secondContext.close();
}
} );

test( 'keeps undo available when meta boxes disable collaboration', async ( {
collaborationUtils,
requestUtils,
admin,
editor,
page,
pageUtils,
} ) => {
const post = await requestUtils.createPost( {
title: 'Meta Box Undo Test',
status: 'draft',
date_gmt: new Date().toISOString(),
} );

await admin.editPost( post.id );
await collaborationUtils.waitForEntityReady( page );
await waitForCollaborationEnabledForCurrentPost( page, false );

await editor.insertBlock( {
name: 'core/paragraph',
attributes: {
content: 'Undo after meta boxes disable collaboration',
},
} );

await expect
.poll( () =>
page.evaluate( () =>
window.wp.data.select( 'core' ).hasUndo()
)
)
.toBe( true );

await pageUtils.pressKeys( 'primary+z' );

await expect.poll( () => editor.getBlocks() ).toHaveLength( 0 );
} );
} );

test.describe( 'RTC-compatible meta boxes', () => {
Expand Down Expand Up @@ -150,23 +205,7 @@ test.describe( 'Collaboration with meta boxes', () => {

// Verify collaboration remains enabled. The RTC-compatible meta
// box should not trigger the incompatibility lock-out.
await page.waitForFunction(
( consent ) => {
const privateApis = ( window as any ).wp.privateApis;
const { unlock } =
privateApis.__dangerousOptInToUnstableAPIsOnlyForCoreModules(
consent,
'@wordpress/core-data'
);
return (
unlock(
window.wp.data.select( 'core/editor' )
).isCollaborationEnabledForCurrentPost() === true
);
},
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
{ timeout: 15000 }
);
await waitForCollaborationEnabledForCurrentPost( page, true );

// Verify a second user can join without seeing the lock modal.
const { page: page2 } = await collaborationUtils.joinUser(
Expand Down
Loading