diff --git a/tests/e2e/cloudinary-video-delivery.spec.js b/tests/e2e/cloudinary-video-delivery.spec.js new file mode 100644 index 00000000..e8f0d113 --- /dev/null +++ b/tests/e2e/cloudinary-video-delivery.spec.js @@ -0,0 +1,249 @@ +/** + * External dependencies + */ +const fs = require( 'fs' ); +const path = require( 'path' ); +const { execSync } = require( 'child_process' ); +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +/** + * Internal dependencies + */ +const { ensureCloudinaryConnected } = require( './utils/connection' ); +const { wpCli, getCliContainer } = require( './utils/wizard' ); + +const FIXTURE_PATH = path.join( __dirname, 'fixtures', 'test-video.mp4' ); + +let cloudName; + +/** + * Per-test scratch space populated by beforeEach. + * + * @type {{ postId: number, attachmentId: number, postLink: string }|null} + */ +let created = null; + +/** + * Assert that a given URL is served by Cloudinary under the expected + * cloud name. We intentionally do not assert specific transformations — + * those are an implementation detail of the plugin and may change. + * + * @param {string} rawUrl The URL to validate. + * @param {string} expectedCloud The cloud name parsed from CLOUDINARY_E2E_URL. + */ +function expectCloudinaryUrl( rawUrl, expectedCloud ) { + let parsed; + try { + parsed = new URL( rawUrl ); + } catch ( e ) { + throw new Error( `URL is not parseable: ${ rawUrl }` ); + } + expect( parsed.host, `host of ${ rawUrl }` ).toBe( 'res.cloudinary.com' ); + expect( + parsed.pathname.startsWith( `/${ expectedCloud }/` ), + `pathname of ${ rawUrl } should start with /${ expectedCloud }/` + ).toBe( true ); +} + +/** + * Set cloudinary_media_display.video_player without disturbing other + * keys, bootstrapping the option if it does not yet exist. + * + * We cannot use `wp option patch` here: in a fresh wp-env the + * `cloudinary_media_display` option is unset, so `get_option` returns + * boolean false and `patch` errors with "Cannot create key ... on data + * type boolean". A merge-based `update_option` handles the missing, + * boolean, and populated-array cases uniformly — `(array) false` is []. + * + * The PHP snippet contains spaces and quotes, so it cannot go through + * the space-joined `wpCli` helper; we build the docker command here. + * + * @param {string} value 'wp' or 'cld'. + */ +function setVideoPlayer( value ) { + const php = + 'update_option("cloudinary_media_display", array_merge((array) get_option("cloudinary_media_display", array()), array("video_player" => "' + + value + + '")));'; + const cmd = [ + 'docker', + 'exec', + getCliContainer(), + 'wp', + 'eval', + `'${ php }'`, + '--allow-root', + ].join( ' ' ); + + execSync( cmd, { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'pipe' ] } ); +} + +test.describe( 'Cloudinary video delivery', () => { + test.beforeAll( () => { + ( { cloudName } = ensureCloudinaryConnected() ); + } ); + + test.beforeEach( async ( { requestUtils } ) => { + // Upload the fixture video via REST. + const file = fs.readFileSync( FIXTURE_PATH ); + const media = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/media', + headers: { + 'Content-Type': 'video/mp4', + 'Content-Disposition': 'attachment; filename="test-video.mp4"', + }, + data: file, + } ); + + const attachmentId = media.id; + const sourceUrl = media.source_url; + + // Create a published post containing a single core/video block + // that references the just-uploaded attachment. + const content = + `\n` + + `
\n` + + ``; + + const post = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/posts', + data: { + status: 'publish', + title: `Cloudinary video e2e ${ Date.now() }`, + content, + }, + } ); + + created = { + postId: post.id, + attachmentId, + postLink: post.link, + }; + + // The plugin's URL rewriting depends on the asset being synced + // to Cloudinary. Drive the sync synchronously here so the + // front-end visit sees rewritten URLs. + wpCli( [ 'cloudinary', 'sync' ] ); + } ); + + test.afterEach( async () => { + if ( ! created ) { + return; + } + const { postId, attachmentId } = created; + created = null; + + // Best-effort cleanup via WP-CLI. Matches the image spec; we + // do not use the REST API because this wp-env runs without + // pretty permalinks, which makes `?force=true` fragile. + try { + wpCli( [ 'post', 'delete', String( postId ), '--force' ] ); + } catch ( e ) { + // eslint-disable-next-line no-console + console.warn( 'Post cleanup failed:', e.message ); + } + try { + wpCli( [ 'post', 'delete', String( attachmentId ), '--force' ] ); + } catch ( e ) { + // eslint-disable-next-line no-console + console.warn( 'Media cleanup failed:', e.message ); + } + } ); + + test.describe( 'with default WP player', () => { + test( 'serves video from a core/video block via Cloudinary', async ( { + page, + } ) => { + expect( + created, + 'post + attachment should be created' + ).not.toBeNull(); + + await page.goto( created.postLink ); + + // Locate the core/video block on the rendered page. + const video = page.locator( 'figure.wp-block-video video' ).first(); + await expect( + video, + 'core/video block should render a