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
2 changes: 2 additions & 0 deletions __mocks__/@wordpress/block-editor.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
// Intentionally empty — prevents the real module from loading.

export const store = { name: 'core/block-editor' };
2 changes: 1 addition & 1 deletion __mocks__/@wordpress/core-data.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
// Intentionally empty — prevents the real module from loading.
export const store = { name: 'core/data' };
11 changes: 11 additions & 0 deletions e2e/editor-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,17 @@ export default class EditorPage {
}, index );
}

/**
* Call the bridge's `getTitleAndContent()` and return the result.
*
* @return {Promise<{title: string, content: string, changed: boolean}>} The editor state.
*/
async getTitleAndContent() {
return await this.#page.evaluate( () =>
window.editor.getTitleAndContent()
);
}

/**
* Retrieve all blocks from the editor via the WP data store.
*
Expand Down
182 changes: 182 additions & 0 deletions e2e/get-title-and-content.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* External dependencies
*/
import { test, expect } from '@playwright/test';

/**
* Internal dependencies
*/
import EditorPage from './editor-page';

test.describe( 'getTitleAndContent', () => {
test( 'returns correct title and content before any edits', async ( {
page,
} ) => {
const editor = new EditorPage( page );
await editor.setup( {
post: {
id: 1,
type: 'post',
status: 'draft',
title: 'Initial Title',
content:
'<!-- wp:paragraph -->\n<p>Hello</p>\n<!-- /wp:paragraph -->',
},
} );

const result = await editor.getTitleAndContent();

expect( typeof result.title ).toBe( 'string' );
expect( typeof result.content ).toBe( 'string' );
expect( result.title ).toBe( 'Initial Title' );
expect( result.content ).toBe(
'<!-- wp:paragraph -->\n<p>Hello</p>\n<!-- /wp:paragraph -->'
);
expect( result.changed ).toBe( false );
} );

test( 'returns plain strings after editing the title', async ( {
page,
} ) => {
const editor = new EditorPage( page );
await editor.setup( {
post: {
id: 1,
type: 'post',
status: 'draft',
title: 'Original',
content: '',
},
} );

const titleInput = page.getByRole( 'textbox', {
name: 'Add title',
} );
await titleInput.click();
await page.keyboard.press( 'ControlOrMeta+a' );
await page.keyboard.type( 'Updated Title' );

const result = await editor.getTitleAndContent();

expect( typeof result.title ).toBe( 'string' );
expect( result.title ).toBe( 'Updated Title' );
expect( result.changed ).toBe( true );
} );

test( 'returns plain strings after editing content', async ( { page } ) => {
const editor = new EditorPage( page );
await editor.setup( {
post: {
id: 1,
type: 'post',
status: 'draft',
title: 'Title',
content: '',
},
} );

await editor.clickBlockAppender();
await page.keyboard.type( 'New paragraph' );

const result = await editor.getTitleAndContent();

expect( typeof result.title ).toBe( 'string' );
expect( typeof result.content ).toBe( 'string' );
expect( result.title ).toBe( 'Title' );
expect( result.content ).toContain( 'New paragraph' );
expect( result.changed ).toBe( true );
} );

test( 'returns plain strings with empty initial state', async ( {
page,
} ) => {
const editor = new EditorPage( page );
await editor.setup();

const result = await editor.getTitleAndContent();

expect( typeof result.title ).toBe( 'string' );
expect( typeof result.content ).toBe( 'string' );
expect( result.title ).toBe( '' );
expect( result.content ).toBe( '' );
expect( result.changed ).toBe( false );
} );

test( 'returns plain strings when data store title is a {raw, rendered} object', async ( {
page,
} ) => {
const editor = new EditorPage( page );
await editor.setup( {
post: {
id: 1,
type: 'post',
status: 'draft',
title: 'Initial Title',
content: '',
},
} );

// Inject an object-shaped title edit via editEntityRecord.
// This simulates the Gutenberg data store bug where
// getEditedPostAttribute bypasses getPostRawValue normalization
// for values in the edits layer.
await page.evaluate( () => {
window.wp.data
.dispatch( 'core' )
.editEntityRecord( 'postType', 'post', 1, {
title: {
raw: 'Object Title',
rendered: '<b>Object Title</b>',
},
} );
} );

const result = await editor.getTitleAndContent();

expect( typeof result.title ).toBe( 'string' );
expect( result.title ).toBe( 'Object Title' );
expect( result.changed ).toBe( true );

// Second call should report no further changes.
const second = await editor.getTitleAndContent();
expect( second.changed ).toBe( false );
} );

test( 'returns plain strings when data store content is a {raw, rendered} object', async ( {
page,
} ) => {
const editor = new EditorPage( page );
await editor.setup( {
post: {
id: 1,
type: 'post',
status: 'draft',
title: 'Title',
content: '',
},
} );

await page.evaluate( () => {
window.wp.data
.dispatch( 'core' )
.editEntityRecord( 'postType', 'post', 1, {
content: {
raw: '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->',
rendered: '<p>Test</p>',
},
} );
} );

const result = await editor.getTitleAndContent();

expect( typeof result.content ).toBe( 'string' );
expect( result.content ).toContain(
'<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->'
);
expect( result.changed ).toBe( true );

// Second call should report no further changes.
const second = await editor.getTitleAndContent();
expect( second.changed ).toBe( false );
} );
} );
16 changes: 10 additions & 6 deletions ios/Sources/GutenbergKit/Sources/EditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,18 +374,22 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
evaluate("editor.setContent('\(escapedString)');", isCritical: true)
}

/// Returns the current editor content.
public func getContent() async throws -> String {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It appears there is one location using this method that we need to update: the unused comment editor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Interesting point – I've put it back with a note explaining why it's there to avoid a breaking change in 3181e08.

guard isReady else { throw EditorNotReadyError() }
return try await webView.evaluateJavaScript("editor.getContent();") as! String
}

public struct EditorTitleAndContent: Decodable {
public let title: String
public let content: String
public let changed: Bool
}

/// Returns just the current editor content, without the title.
///
/// Use this when the editor is used without a title field (e.g. as a
/// comment editor). Delegates to `getTitleAndContent()` internally so
/// the same normalization is applied.
public func getContent() async throws -> String {
let result = try await getTitleAndContent()
return result.content
}

/// Returns the current editor title and content.
public func getTitleAndContent() async throws -> EditorTitleAndContent {
guard isReady else { throw EditorNotReadyError() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public protocol EditorViewControllerDelegate: AnyObject {

/// Notifies the client about the new edits.
///
/// - note: To get the latest content, call ``EditorViewController/getContent()``.
/// - note: To get the latest content, call ``EditorViewController/getTitleAndContent()``.
/// Retrieving the content is a relatively expensive operation and should not
/// be performed too frequently during editing.
///
Expand Down
Loading
Loading