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
17 changes: 7 additions & 10 deletions e2e/embed-content.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ test.describe( 'Embedded Content', () => {
expect( blocks[ 0 ].attributes.providerNameSlug ).toBe( 'youtube' );
} );

test( 'should show a fallback link for a non-embeddable URL', async ( {
test( 'should store no preview for a non-embeddable URL', async ( {
page,
} ) => {
const editor = new EditorPage( page );
Expand All @@ -123,16 +123,13 @@ test.describe( 'Embedded Content', () => {
await urlInput.fill( 'https://example.com/not-embeddable' );
await page.keyboard.press( 'Enter' );

// Wait for the embed to resolve (or fall back).
// The block should still render with the URL attribute.
// The proxy answers 404, so core stores `false` for the preview
// rather than a preview object.
await page.waitForFunction(
() => {
const blocks = window.wp.data
.select( 'core/block-editor' )
.getBlocks();
return blocks[ 0 ]?.attributes?.url;
},
null,
( url ) =>
window.wp.data.select( 'core' ).getEmbedPreview( url ) ===
false,
'https://example.com/not-embeddable',
{ timeout: 30_000 }
);

Expand Down
66 changes: 22 additions & 44 deletions src/utils/api-fetch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import apiFetch from '@wordpress/api-fetch';
import { getQueryArg } from '@wordpress/url';
import { __ } from '@wordpress/i18n';
import { getGBKit, POST_FALLBACKS } from './bridge';
import { info, warn, error as logError } from './logger';
Expand Down Expand Up @@ -454,56 +453,35 @@ function mediaPermissionsMiddleware( options, next ) {
* Remove the wrapping element from the oEmbed response, as it breaks
* Gutenberg's sizing styles.
*
* A failed request is left to reject, so core stores `false` and blocks that
* poll for a preview (e.g. VideoPress while processing an upload) ask again.
*
* @type {APIFetchMiddleware}
*
* @todo Hoist this host-specific logic to the host app.
*/
function transformOEmbedApiResponse( options, next ) {
if ( options.path && options.path.indexOf( 'oembed' ) !== -1 ) {
const url = getQueryArg( options.path, 'url' );
const response = next( options, next );

/**
* Creates an embed response emulating core's fallback link.
*/
function createFallbackResponse() {
const link = document.createElement( 'a' );
link.href = url;
link.innerText = url;
return {
html: link.outerHTML,
type: 'rich',
provider_name: 'Embed',
};
}

return new Promise( ( resolve ) => {
response
.then( ( data ) => {
if ( data.html ) {
/**
* Removes wrappers from YouTube, Vimeo, Dailymotion, TED block, e.g.
* <span class="embed-youtube">, <div class="embed-vimeo">, <div class="embed-dailymotion">, <div class="embed-ted">
* and return just the <iframe> child directly to allow wide & full width sizing.
*/
const doc =
document.implementation.createHTMLDocument( '' );
doc.body.innerHTML = data.html;
const selectors = [
'[class="embed-youtube"]',
'[class="embed-vimeo"]',
'[class="embed-dailymotion"]',
'[class="embed-ted"]',
].join( ',' );
const wrapper = doc.querySelector( selectors );
data.html = wrapper ? wrapper.innerHTML : data.html;
}
return next( options, next ).then( ( data ) => {
if ( data?.html ) {
/**
* Removes wrappers from YouTube, Vimeo, Dailymotion, TED block, e.g.
* <span class="embed-youtube">, <div class="embed-vimeo">, <div class="embed-dailymotion">, <div class="embed-ted">
* and return just the <iframe> child directly to allow wide & full width sizing.
*/
const doc = document.implementation.createHTMLDocument( '' );
doc.body.innerHTML = data.html;
const selectors = [
'[class="embed-youtube"]',
'[class="embed-vimeo"]',
'[class="embed-dailymotion"]',
'[class="embed-ted"]',
].join( ',' );
const wrapper = doc.querySelector( selectors );
data.html = wrapper ? wrapper.innerHTML : data.html;
}

resolve( data );
} )
.catch( () => {
resolve( createFallbackResponse() );
} );
return data;
} );
}

Expand Down
73 changes: 73 additions & 0 deletions src/utils/api-fetch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -743,4 +743,77 @@ describe( 'api-fetch credentials handling', () => {
expect( options.headers[ 'X-Custom-Header' ] ).toBe( 'custom-value' );
expect( options.headers.Authorization ).toBe( 'Bearer preserve-test' );
} );

describe( 'oEmbed responses', () => {
const proxyPath =
'/oembed/1.0/proxy?url=https%3A%2F%2Fvideopress.com%2Fv%2FeDeLfBNN';

beforeEach( () => {
bridge.getGBKit.mockReturnValue( {
siteApiRoot: 'https://example.com/wp-json/',
authHeader: 'Bearer test-token',
siteApiNamespace: [],
namespaceExcludedPaths: [],
} );
} );

function respondWith( body, status = 200 ) {
global.fetch = vi.fn( () =>
Promise.resolve(
new Response( JSON.stringify( body ), {
status,
headers: { 'Content-Type': 'application/json' },
} )
)
);
}

it( 'rejects when WordPress cannot embed the URL', async () => {

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.

Finding from Claude:

The E2E test should show a fallback link for a non-embeddable URL (e2e/embed-content.spec.js:110) still describes the removed fallback, and it passes with this fix reverted: it only waits for attributes.url, which is set on submit, before the request settles. Rename it and assert the stored preview so it guards this change?

await page.waitForFunction(
  (url) => window.wp.data.select("core").getEmbedPreview(url) === false,
  "https://example.com/not-embeddable",
  { timeout: 30_000 },
);

respondWith(
{
code: 'oembed_invalid_url',
message: 'Not Found',
data: { status: 404 },
},
404
);

await expect(
apiFetch( { path: proxyPath } )
).rejects.toMatchObject( { code: 'oembed_invalid_url' } );
} );

it( 'rejects when the request fails', async () => {
global.fetch = vi.fn( () =>
Promise.reject( new TypeError( 'Load failed' ) )
);

await expect(
apiFetch( { path: proxyPath } )
).rejects.toBeDefined();
} );

it( 'removes the wrapper around a provider iframe', async () => {
respondWith( {
html: '<span class="embed-youtube"><iframe src="https://www.youtube.com/embed/abc"></iframe></span>',
type: 'video',
} );

const preview = await apiFetch( { path: proxyPath } );

expect( preview.html ).toBe(
'<iframe src="https://www.youtube.com/embed/abc"></iframe>'
);
} );

it( 'returns other embed markup as WordPress sent it', async () => {
const html =
'<a href="https://example.com/post">https://example.com/post</a>';
respondWith( { html, type: 'rich' } );

const preview = await apiFetch( { path: proxyPath } );

expect( preview ).toEqual( { html, type: 'rich' } );
} );
} );
} );
Loading