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
220 changes: 220 additions & 0 deletions e2e/post-entity-seeding.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { test, expect } from '@playwright/test';
import EditorPage from './editor-page';
import { credentials } from './wp-env-fixtures';
import { fetchJson } from './fetch-json';

/** Matches the post's own path, not sub-resources like its autosaves. */
const POST_PATH = /\/wp\/v2\/posts\/1(?:[?&]|$)/;

/** Matches the post's path and its sub-resources. */
const POST_PATHS = /\/wp\/v2\/posts\/1(?:[/?&]|$)/;

const HOST_POST = {
id: 1,
type: 'post',
status: 'draft',
title: 'Host title',
content:
'<!-- wp:paragraph -->\n<p>Host content</p>\n<!-- /wp:paragraph -->',
};

test.describe( 'Post entity seeding', () => {
let serverPost;

test.beforeAll( async () => {
serverPost = await fetchJson(
`${ credentials.siteApiRoot }wp/v2/posts/1?context=edit`,
credentials,
'post'
);
} );

test( 'seeds the preloaded post without requesting it', async ( {
page,
} ) => {
const postRequests = [];
page.on( 'request', ( request ) => {
if ( POST_PATH.test( decodeURIComponent( request.url() ) ) ) {
postRequests.push( `${ request.method() } ${ request.url() }` );
}
} );

// Stand in for the Android bridge, which receives host events on
// `window.editorDelegate`.
await page.addInitScript( () => {
window.contentChangedCount = 0;
window.featuredImageChanges = [];
window.editorDelegate = {
onEditorContentChanged: () => window.contentChangedCount++,
onEditorFeaturedImageChanged: ( mediaID ) =>
window.featuredImageChanges.push( mediaID ),
};
} );

const editor = new EditorPage( page );
await editor.setup( {
post: HOST_POST,
preloadData: {
'/wp/v2/posts/1?context=edit': {
// The host may hold a different, unsynced featured image.
body: { ...serverPost, featured_media: 123 },
headers: {},
},
},
} );

await expect(
page.getByRole( 'textbox', { name: 'Add title' } )
).toHaveText( 'Host title' );
await expect( page.getByText( 'Host content' ) ).toBeVisible();

const record = await page.evaluate( () =>
window.wp.data
.select( 'core' )
.getEntityRecord( 'postType', 'post', 1 )
);
expect( record.date ).toBe( serverPost.date );
expect( record.author ).toBe( serverPost.author );
expect( record.title.raw ).toBe( 'Host title' );

expect(
await page.evaluate( () =>
window.wp.data.select( 'core/editor' ).isEditedPostDirty()
)
).toBe( false );
expect( await page.evaluate( () => window.contentChangedCount ) ).toBe(
0
);
expect(
await page.evaluate( () => window.featuredImageChanges )
).toEqual( [] );
expect( postRequests ).toEqual( [] );

await page.getByText( 'Host content' ).click();
await page.keyboard.type( ' edited' );

await expect
.poll( () => page.evaluate( () => window.contentChangedCount ) )
.toBeGreaterThan( 0 );
} );

test( "uses core's post type entity configurations", async ( { page } ) => {
const editor = new EditorPage( page );
await editor.setup( { post: HOST_POST } );

const config = await page.evaluate( () => {
const configs = window.wp.data
.select( 'core' )
.getEntitiesConfig( 'postType' )
.filter( ( { name } ) =>
[ 'post', 'attachment', 'wp_block' ].includes( name )
);
return configs.map( ( entity ) => ( {
name: entity.name,
baseURL: entity.baseURL,
baseURLParams: entity.baseURLParams,
prePersist: typeof entity.__unstablePrePersist,
} ) );
} );

expect( config ).toEqual(
[
[ 'post', '/wp/v2/posts' ],
[ 'attachment', '/wp/v2/media' ],
[ 'wp_block', '/wp/v2/blocks' ],
].map( ( [ name, baseURL ] ) => ( {
name,
baseURL,
baseURLParams: { context: 'edit' },
prePersist: 'function',
} ) )
);
} );

test( 'renders the post when the post types fail to load', async ( {
page,
} ) => {
await page.route( /\/wp\/v2\/types\?/, ( route ) => route.abort() );

const editor = new EditorPage( page );
await editor.setup( { post: HOST_POST, preloadData: {} } );

await expect(
page.getByRole( 'textbox', { name: 'Add title' } )
).toHaveText( 'Host title' );
await expect( page.getByText( 'Host content' ) ).toBeVisible();
} );

test( "keeps the host's title and content when the post is fetched", async ( {
page,
} ) => {
const editor = new EditorPage( page );
await editor.setup( { post: HOST_POST } );

const records = await page.evaluate( async () => {
const { resolveSelect, select } = window.wp.data;
const { getEntityRecord, getEntityRecords } =
resolveSelect( 'core' );
await getEntityRecord( 'postType', 'post', 1, { _embed: true } );
await getEntityRecords( 'postType', 'post', { include: [ 1 ] } );
return {
embedded: select( 'core' ).getEntityRecord(
'postType',
'post',
1,
{ _embed: true }
),
listed: select( 'core' ).getEntityRecords( 'postType', 'post', {
include: [ 1 ],
} )[ 0 ],
edited: select( 'core/editor' ).getEditedPostAttribute(
'title'
),
dirty: select( 'core/editor' ).isEditedPostDirty(),
};
} );

for ( const record of [ records.embedded, records.listed ] ) {
expect( record.title.raw ).toBe( 'Host title' );
expect( record.content.raw ).toBe( HOST_POST.content );
expect( record.title.rendered ).toBe( serverPost.title.rendered );
}
expect( records.embedded._embedded ).toBeDefined();
expect( records.edited ).toBe( 'Host title' );
expect( records.dirty ).toBe( false );
await expect(
page.getByRole( 'textbox', { name: 'Add title' } )
).toHaveText( 'Host title' );
} );

test( 'blocks saving the post from the editor', async ( { page } ) => {
const writeRequests = [];
page.on( 'request', ( request ) => {
if (
request.method() !== 'GET' &&
request.method() !== 'OPTIONS' &&
POST_PATHS.test( decodeURIComponent( request.url() ) )
) {
writeRequests.push( request.url() );
}
} );

const editor = new EditorPage( page );
await editor.setup( { post: HOST_POST } );

await page.getByText( 'Host content' ).click();
await page.keyboard.type( ' edited' );

const result = await page.evaluate( async () => {
const { dispatch, select } = window.wp.data;
await dispatch( 'core/editor' ).savePost();
return {
failed: select( 'core/editor' ).didPostSaveRequestFail(),
dirty: select( 'core/editor' ).isEditedPostDirty(),
};
} );

expect( result ).toEqual( { failed: true, dirty: true } );
expect( writeRequests ).toEqual( [] );
} );
} );
2 changes: 0 additions & 2 deletions src/components/editor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useSyncSaveAvailability } from './use-sync-save-availability';
import { useHostBridge } from './use-host-bridge';
import { useEditorReady } from './use-editor-ready';
import { useHostExceptionLogging } from './use-host-exception-logging';
import { useEditorSetup } from './use-editor-setup';
import { useMediaUpload } from './use-media-upload';
import TextEditor from '../text-editor';
import { useSyncFeaturedImage } from './use-sync-featured-image';
Expand Down Expand Up @@ -37,7 +36,6 @@ export default function Editor( { post, children, hideTitle } ) {
useSyncHistoryControls();
useSyncSaveAvailability();
useHostExceptionLogging();
useEditorSetup( post );
useMediaUpload();
useDevModeNotice();
useAtAutocompleter();
Expand Down
61 changes: 61 additions & 0 deletions src/components/editor/test/use-sync-featured-image.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useSyncFeaturedImage } from '../use-sync-featured-image';
import { onEditorFeaturedImageChanged } from '../../../utils/bridge';

const mockGetEditedPostAttribute = vi.fn();

vi.mock( '@wordpress/data', () => ( {
useSelect: ( mapSelect ) =>
mapSelect( () => ( {
getEditedPostAttribute: mockGetEditedPostAttribute,
} ) ),
} ) );
vi.mock( '@wordpress/editor' );
vi.mock( '../../../utils/bridge' );

describe( 'useSyncFeaturedImage', () => {
beforeEach( () => {
vi.clearAllMocks();
} );

it( 'does not report the featured image the post loaded with', () => {
mockGetEditedPostAttribute.mockReturnValue( 123 );

renderHook( () => useSyncFeaturedImage() );

expect( onEditorFeaturedImageChanged ).not.toHaveBeenCalled();
} );

it( 'reports a featured image set in the editor', () => {
mockGetEditedPostAttribute.mockReturnValue( 123 );
const { rerender } = renderHook( () => useSyncFeaturedImage() );

mockGetEditedPostAttribute.mockReturnValue( 456 );
rerender();

expect( onEditorFeaturedImageChanged ).toHaveBeenCalledWith( 456 );
} );

it( 'reports the initial featured image when set again in the editor', () => {
mockGetEditedPostAttribute.mockReturnValue( 123 );
const { rerender } = renderHook( () => useSyncFeaturedImage() );

mockGetEditedPostAttribute.mockReturnValue( 456 );
rerender();
mockGetEditedPostAttribute.mockReturnValue( 123 );
rerender();

expect( onEditorFeaturedImageChanged ).toHaveBeenLastCalledWith( 123 );
} );

it( 'does not report a removed featured image', () => {
mockGetEditedPostAttribute.mockReturnValue( 123 );
const { rerender } = renderHook( () => useSyncFeaturedImage() );

mockGetEditedPostAttribute.mockReturnValue( 0 );
rerender();

expect( onEditorFeaturedImageChanged ).not.toHaveBeenCalled();
} );
} );
60 changes: 0 additions & 60 deletions src/components/editor/use-editor-setup.js

This file was deleted.

12 changes: 11 additions & 1 deletion src/components/editor/use-sync-featured-image.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useEffect } from '@wordpress/element';
import { useEffect, useRef } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { onEditorFeaturedImageChanged } from '../../utils/bridge';

/**
* Unidirectionally synchronizes the featured image with the host.
*
* Only changes made in the editor are reported. The initial value comes from
* the preloaded server record, which may predate the host's own edits.
*/
export function useSyncFeaturedImage() {
const { featuredImageId } = useSelect( ( select ) => {
Expand All @@ -13,8 +16,15 @@ export function useSyncFeaturedImage() {
featuredImageId: getEditedPostAttribute( 'featured_media' ),
};
}, [] );
const lastFeaturedImageId = useRef( featuredImageId );

useEffect( () => {
if ( featuredImageId === lastFeaturedImageId.current ) {
return;
}

lastFeaturedImageId.current = featuredImageId;

if ( ! featuredImageId ) {
return;
}
Expand Down
Loading
Loading