From 66715b975612388b1672fad34c6df4c7e2c214cd Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Tue, 13 Oct 2026 14:38:21 +0530 Subject: [PATCH 1/2] test(e2e): Add per-asset overlay and crop spec Drive the real Edit Asset UI to save a text overlay and a crop, then assert the front-end image delivery URL picks them up and reverts to the default once they are removed. --- tests/e2e/cloudinary-asset-edit.spec.js | 253 ++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 tests/e2e/cloudinary-asset-edit.spec.js diff --git a/tests/e2e/cloudinary-asset-edit.spec.js b/tests/e2e/cloudinary-asset-edit.spec.js new file mode 100644 index 000000000..7e2142be9 --- /dev/null +++ b/tests/e2e/cloudinary-asset-edit.spec.js @@ -0,0 +1,253 @@ +/** + * External dependencies + */ +const fs = require( 'fs' ); +const path = require( 'path' ); +const { test, expect } = require( './fixtures' ); + +/** + * Internal dependencies + */ +const { ensureCloudinaryConnected } = require( './utils/connection' ); +const { expectCloudinaryUrl } = require( './utils/delivery' ); +const { wpCli } = require( './utils/wizard' ); + +const FIXTURE_PATH = path.join( __dirname, 'fixtures', 'test-image.jpg' ); + +const OVERLAY_TEXT = 'CldE2EOverlay'; +const CROP = 'c_crop,h_120,w_160'; + +let cloudName; + +/** + * State shared by the tests in this file. Every test edits the same + * attachment, so the tests run in order (see `mode: 'serial'`). + * + * @type {{ postId: number, postLink: string, attachmentId: number }|null} + */ +let created = null; + +/** + * Locate an Edit Asset field by its setting slug. The plugin's UI builds + * IDs such as `edit_asset.edit_affects.text_overlay_text`, which contain + * dots, so an attribute selector is used instead of `#id`. + * + * @param {import('@playwright/test').Page} page + * @param {string} slug Setting slug. + * @return {import('@playwright/test').Locator} The input. + */ +function assetField( page, slug ) { + return page.locator( `[id="edit_asset.edit_affects.${ slug }"]` ); +} + +/** + * Open the Edit Asset page for the test attachment and wait for the + * preview script to initialise. + * + * @param {import('@playwright/test').Page} page + */ +async function openEditAsset( page ) { + await page.goto( + `/wp-admin/admin.php?page=cloudinary§ion=edit-asset&asset=${ created.attachmentId }` + ); + + // `asset-edit.js` appends the preview image on `window.load`; once it + // is there, the save handlers are bound. + await expect( + page.locator( '#cld-asset-edit img' ), + 'the asset preview should render' + ).toBeVisible(); +} + +/** + * Click a save button on the Edit Asset page and wait for the REST call. + * + * @param {import('@playwright/test').Page} page + * @param {import('@playwright/test').Locator} button + * @return {Promise} The `save_asset` response body. + */ +async function saveAsset( page, button ) { + const [ response ] = await Promise.all( [ + page.waitForResponse( + ( res ) => + res.url().includes( 'save_asset' ) && + res.request().method() === 'POST' + ), + button.click(), + ] ); + expect( response.ok(), 'save_asset should succeed' ).toBe( true ); + + const body = await response.json(); + expect( + body.note, + 'the plugin should accept the transformation as valid' + ).toBeUndefined(); + + return body; +} + +/** + * Open the published post and read the delivery attributes of the inline + * image once the lazy-loader has swapped in the Cloudinary URL. + * + * @param {import('@playwright/test').Page} page + * @return {Promise<{ src: string, transformations: string }>} Delivery data. + */ +async function readDelivery( page ) { + await page.goto( created.postLink ); + + const img = page + .locator( `.wp-block-image img.wp-image-${ created.attachmentId }` ) + .first(); + await expect( img, 'the inline image should render' ).toBeVisible(); + await img.scrollIntoViewIfNeeded(); + + await expect( + img, + 'the lazy-loader should swap in the Cloudinary URL' + ).toHaveAttribute( 'src', /^https:\/\/res\.cloudinary\.com\// ); + + const src = await img.getAttribute( 'src' ); + expectCloudinaryUrl( src, cloudName ); + + return { + src, + transformations: + ( await img.getAttribute( 'data-transformations' ) ) || '', + }; +} + +// @serial: needs real credentials in `cloudinary_connect` to sync assets, +// while every analytics spec overwrites that option with fake ones. +test.describe( 'Cloudinary per-asset editing', { tag: '@serial' }, () => { + test.describe.configure( { mode: 'serial' } ); + + test.beforeAll( async ( { requestUtils } ) => { + ( { cloudName } = ensureCloudinaryConnected() ); + + const media = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/media', + headers: { + 'Content-Type': 'image/jpeg', + 'Content-Disposition': `attachment; filename="asset-edit-e2e-${ Date.now() }.jpg"`, + }, + data: fs.readFileSync( FIXTURE_PATH ), + } ); + + const content = + `\n` + + `
\n` + + ``; + + const post = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/posts', + data: { + status: 'publish', + title: `Cloudinary asset edit e2e ${ Date.now() }`, + content, + }, + } ); + + created = { + postId: post.id, + postLink: post.link, + attachmentId: media.id, + }; + + // The Edit Asset page and the front-end rewrite both need the + // asset to be synced to Cloudinary. + wpCli( [ 'cloudinary', 'sync' ] ); + } ); + + test.afterAll( () => { + if ( ! created ) { + return; + } + const { postId, attachmentId } = created; + created = null; + + // WP-CLI rather than REST for the same reason as the image delivery + // spec: `force=true` on a `?rest_route=` URL is fragile. + for ( const id of [ postId, attachmentId ] ) { + try { + wpCli( [ 'post', 'delete', String( id ), '--force' ] ); + } catch ( e ) { + console.warn( `Cleanup of post ${ id } failed:`, e.message ); + } + } + } ); + + test( 'text overlay updates the delivery URL and reverts when removed', async ( { + page, + } ) => { + const baseline = await readDelivery( page ); + expect( baseline.src ).not.toContain( 'l_text:' ); + + await openEditAsset( page ); + await assetField( page, 'text_overlay_text' ).fill( OVERLAY_TEXT ); + const saved = await saveAsset( + page, + page.locator( '#cld-asset-save-text-overlay' ) + ); + expect( saved.text_overlay ).toContain( OVERLAY_TEXT ); + + const withOverlay = await readDelivery( page ); + for ( const value of [ + withOverlay.transformations, + withOverlay.src, + ] ) { + expect( value ).toContain( `l_text:Arial_20:${ OVERLAY_TEXT }` ); + expect( value ).toContain( 'fl_layer_apply' ); + } + + // "Reset All" only clears the form; saving persists the empty overlay. + await openEditAsset( page ); + await expect( assetField( page, 'text_overlay_text' ) ).toHaveValue( + OVERLAY_TEXT + ); + await page.locator( '#cld-asset-remove-text-overlay' ).click(); + await expect( assetField( page, 'text_overlay_text' ) ).toHaveValue( + '' + ); + const cleared = await saveAsset( + page, + page.locator( '#cld-asset-save-text-overlay' ) + ); + expect( cleared.text_overlay ).toBe( '' ); + + expect( await readDelivery( page ) ).toEqual( baseline ); + } ); + + test( 'crop updates the delivery URL and reverts when removed', async ( { + page, + } ) => { + const baseline = await readDelivery( page ); + expect( baseline.src ).not.toContain( CROP ); + + await openEditAsset( page ); + const input = assetField( page, 'transformations' ); + await input.fill( CROP ); + const saved = await saveAsset( + page, + page.locator( '#cld-asset-edit-save' ) + ); + expect( saved.transformations ).toBe( CROP ); + + const withCrop = await readDelivery( page ); + expect( withCrop.transformations ).toContain( CROP ); + expect( withCrop.src ).toContain( CROP ); + + await openEditAsset( page ); + await expect( input ).toHaveValue( CROP ); + await input.fill( '' ); + const cleared = await saveAsset( + page, + page.locator( '#cld-asset-edit-save' ) + ); + expect( cleared.transformations ).toBe( '' ); + + expect( await readDelivery( page ) ).toEqual( baseline ); + } ); +} ); From 42ba66626b931c6845f863257605f858cbb50374 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Tue, 13 Oct 2026 15:05:21 +0530 Subject: [PATCH 2/2] test(e2e): Read delivered URL from currentSrc When the theme emits a srcset, the lazy-loader restores it and keeps the SVG placeholder in src, so the src check failed in CI. Assert on currentSrc, which is the URL the browser loads in both cases. --- tests/e2e/cloudinary-asset-edit.spec.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/e2e/cloudinary-asset-edit.spec.js b/tests/e2e/cloudinary-asset-edit.spec.js index 7e2142be9..ca4eb3d94 100644 --- a/tests/e2e/cloudinary-asset-edit.spec.js +++ b/tests/e2e/cloudinary-asset-edit.spec.js @@ -88,7 +88,8 @@ async function saveAsset( page, button ) { /** * Open the published post and read the delivery attributes of the inline - * image once the lazy-loader has swapped in the Cloudinary URL. + * image once the lazy-loader has swapped in the Cloudinary URL. `src` in + * the result is the URL the browser loaded. * * @param {import('@playwright/test').Page} page * @return {Promise<{ src: string, transformations: string }>} Delivery data. @@ -102,12 +103,18 @@ async function readDelivery( page ) { await expect( img, 'the inline image should render' ).toBeVisible(); await img.scrollIntoViewIfNeeded(); - await expect( - img, - 'the lazy-loader should swap in the Cloudinary URL' - ).toHaveAttribute( 'src', /^https:\/\/res\.cloudinary\.com\// ); - - const src = await img.getAttribute( 'src' ); + // The lazy-loader either writes the delivery URL to `src` or, when the + // theme emits a srcset, restores `data-srcset` and leaves the SVG + // placeholder in `src`. `currentSrc` is the URL the browser picked in + // both cases. + const currentSrc = () => img.evaluate( ( node ) => node.currentSrc ); + await expect + .poll( currentSrc, { + message: 'the lazy-loader should swap in the Cloudinary URL', + } ) + .toMatch( /^https:\/\/res\.cloudinary\.com\// ); + + const src = await currentSrc(); expectCloudinaryUrl( src, cloudName ); return {