|
| 1 | +/** |
| 2 | + * External dependencies |
| 3 | + */ |
| 4 | +const fs = require( 'fs' ); |
| 5 | +const path = require( 'path' ); |
| 6 | +const { test, expect } = require( './fixtures' ); |
| 7 | + |
| 8 | +/** |
| 9 | + * Internal dependencies |
| 10 | + */ |
| 11 | +const { ensureCloudinaryConnected } = require( './utils/connection' ); |
| 12 | +const { expectCloudinaryUrl } = require( './utils/delivery' ); |
| 13 | +const { wpCli } = require( './utils/wizard' ); |
| 14 | + |
| 15 | +const FIXTURE_PATH = path.join( __dirname, 'fixtures', 'test-image.jpg' ); |
| 16 | + |
| 17 | +const OVERLAY_TEXT = 'CldE2EOverlay'; |
| 18 | +const CROP = 'c_crop,h_120,w_160'; |
| 19 | + |
| 20 | +let cloudName; |
| 21 | + |
| 22 | +/** |
| 23 | + * State shared by the tests in this file. Every test edits the same |
| 24 | + * attachment, so the tests run in order (see `mode: 'serial'`). |
| 25 | + * |
| 26 | + * @type {{ postId: number, postLink: string, attachmentId: number }|null} |
| 27 | + */ |
| 28 | +let created = null; |
| 29 | + |
| 30 | +/** |
| 31 | + * Locate an Edit Asset field by its setting slug. The plugin's UI builds |
| 32 | + * IDs such as `edit_asset.edit_affects.text_overlay_text`, which contain |
| 33 | + * dots, so an attribute selector is used instead of `#id`. |
| 34 | + * |
| 35 | + * @param {import('@playwright/test').Page} page |
| 36 | + * @param {string} slug Setting slug. |
| 37 | + * @return {import('@playwright/test').Locator} The input. |
| 38 | + */ |
| 39 | +function assetField( page, slug ) { |
| 40 | + return page.locator( `[id="edit_asset.edit_affects.${ slug }"]` ); |
| 41 | +} |
| 42 | + |
| 43 | +/** |
| 44 | + * Open the Edit Asset page for the test attachment and wait for the |
| 45 | + * preview script to initialise. |
| 46 | + * |
| 47 | + * @param {import('@playwright/test').Page} page |
| 48 | + */ |
| 49 | +async function openEditAsset( page ) { |
| 50 | + await page.goto( |
| 51 | + `/wp-admin/admin.php?page=cloudinary§ion=edit-asset&asset=${ created.attachmentId }` |
| 52 | + ); |
| 53 | + |
| 54 | + // `asset-edit.js` appends the preview image on `window.load`; once it |
| 55 | + // is there, the save handlers are bound. |
| 56 | + await expect( |
| 57 | + page.locator( '#cld-asset-edit img' ), |
| 58 | + 'the asset preview should render' |
| 59 | + ).toBeVisible(); |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Click a save button on the Edit Asset page and wait for the REST call. |
| 64 | + * |
| 65 | + * @param {import('@playwright/test').Page} page |
| 66 | + * @param {import('@playwright/test').Locator} button |
| 67 | + * @return {Promise<Object>} The `save_asset` response body. |
| 68 | + */ |
| 69 | +async function saveAsset( page, button ) { |
| 70 | + const [ response ] = await Promise.all( [ |
| 71 | + page.waitForResponse( |
| 72 | + ( res ) => |
| 73 | + res.url().includes( 'save_asset' ) && |
| 74 | + res.request().method() === 'POST' |
| 75 | + ), |
| 76 | + button.click(), |
| 77 | + ] ); |
| 78 | + expect( response.ok(), 'save_asset should succeed' ).toBe( true ); |
| 79 | + |
| 80 | + const body = await response.json(); |
| 81 | + expect( |
| 82 | + body.note, |
| 83 | + 'the plugin should accept the transformation as valid' |
| 84 | + ).toBeUndefined(); |
| 85 | + |
| 86 | + return body; |
| 87 | +} |
| 88 | + |
| 89 | +/** |
| 90 | + * Open the published post and read the delivery attributes of the inline |
| 91 | + * image once the lazy-loader has swapped in the Cloudinary URL. `src` in |
| 92 | + * the result is the URL the browser loaded. |
| 93 | + * |
| 94 | + * @param {import('@playwright/test').Page} page |
| 95 | + * @return {Promise<{ src: string, transformations: string }>} Delivery data. |
| 96 | + */ |
| 97 | +async function readDelivery( page ) { |
| 98 | + await page.goto( created.postLink ); |
| 99 | + |
| 100 | + const img = page |
| 101 | + .locator( `.wp-block-image img.wp-image-${ created.attachmentId }` ) |
| 102 | + .first(); |
| 103 | + await expect( img, 'the inline image should render' ).toBeVisible(); |
| 104 | + await img.scrollIntoViewIfNeeded(); |
| 105 | + |
| 106 | + // The lazy-loader either writes the delivery URL to `src` or, when the |
| 107 | + // theme emits a srcset, restores `data-srcset` and leaves the SVG |
| 108 | + // placeholder in `src`. `currentSrc` is the URL the browser picked in |
| 109 | + // both cases. |
| 110 | + const currentSrc = () => img.evaluate( ( node ) => node.currentSrc ); |
| 111 | + await expect |
| 112 | + .poll( currentSrc, { |
| 113 | + message: 'the lazy-loader should swap in the Cloudinary URL', |
| 114 | + } ) |
| 115 | + .toMatch( /^https:\/\/res\.cloudinary\.com\// ); |
| 116 | + |
| 117 | + const src = await currentSrc(); |
| 118 | + expectCloudinaryUrl( src, cloudName ); |
| 119 | + |
| 120 | + return { |
| 121 | + src, |
| 122 | + transformations: |
| 123 | + ( await img.getAttribute( 'data-transformations' ) ) || '', |
| 124 | + }; |
| 125 | +} |
| 126 | + |
| 127 | +// @serial: needs real credentials in `cloudinary_connect` to sync assets, |
| 128 | +// while every analytics spec overwrites that option with fake ones. |
| 129 | +test.describe( 'Cloudinary per-asset editing', { tag: '@serial' }, () => { |
| 130 | + test.describe.configure( { mode: 'serial' } ); |
| 131 | + |
| 132 | + test.beforeAll( async ( { requestUtils } ) => { |
| 133 | + ( { cloudName } = ensureCloudinaryConnected() ); |
| 134 | + |
| 135 | + const media = await requestUtils.rest( { |
| 136 | + method: 'POST', |
| 137 | + path: '/wp/v2/media', |
| 138 | + headers: { |
| 139 | + 'Content-Type': 'image/jpeg', |
| 140 | + 'Content-Disposition': `attachment; filename="asset-edit-e2e-${ Date.now() }.jpg"`, |
| 141 | + }, |
| 142 | + data: fs.readFileSync( FIXTURE_PATH ), |
| 143 | + } ); |
| 144 | + |
| 145 | + const content = |
| 146 | + `<!-- wp:image {"id":${ media.id }} -->\n` + |
| 147 | + `<figure class="wp-block-image"><img src="${ media.source_url }" alt="" class="wp-image-${ media.id }"/></figure>\n` + |
| 148 | + `<!-- /wp:image -->`; |
| 149 | + |
| 150 | + const post = await requestUtils.rest( { |
| 151 | + method: 'POST', |
| 152 | + path: '/wp/v2/posts', |
| 153 | + data: { |
| 154 | + status: 'publish', |
| 155 | + title: `Cloudinary asset edit e2e ${ Date.now() }`, |
| 156 | + content, |
| 157 | + }, |
| 158 | + } ); |
| 159 | + |
| 160 | + created = { |
| 161 | + postId: post.id, |
| 162 | + postLink: post.link, |
| 163 | + attachmentId: media.id, |
| 164 | + }; |
| 165 | + |
| 166 | + // The Edit Asset page and the front-end rewrite both need the |
| 167 | + // asset to be synced to Cloudinary. |
| 168 | + wpCli( [ 'cloudinary', 'sync' ] ); |
| 169 | + } ); |
| 170 | + |
| 171 | + test.afterAll( () => { |
| 172 | + if ( ! created ) { |
| 173 | + return; |
| 174 | + } |
| 175 | + const { postId, attachmentId } = created; |
| 176 | + created = null; |
| 177 | + |
| 178 | + // WP-CLI rather than REST for the same reason as the image delivery |
| 179 | + // spec: `force=true` on a `?rest_route=` URL is fragile. |
| 180 | + for ( const id of [ postId, attachmentId ] ) { |
| 181 | + try { |
| 182 | + wpCli( [ 'post', 'delete', String( id ), '--force' ] ); |
| 183 | + } catch ( e ) { |
| 184 | + console.warn( `Cleanup of post ${ id } failed:`, e.message ); |
| 185 | + } |
| 186 | + } |
| 187 | + } ); |
| 188 | + |
| 189 | + test( 'text overlay updates the delivery URL and reverts when removed', async ( { |
| 190 | + page, |
| 191 | + } ) => { |
| 192 | + const baseline = await readDelivery( page ); |
| 193 | + expect( baseline.src ).not.toContain( 'l_text:' ); |
| 194 | + |
| 195 | + await openEditAsset( page ); |
| 196 | + await assetField( page, 'text_overlay_text' ).fill( OVERLAY_TEXT ); |
| 197 | + const saved = await saveAsset( |
| 198 | + page, |
| 199 | + page.locator( '#cld-asset-save-text-overlay' ) |
| 200 | + ); |
| 201 | + expect( saved.text_overlay ).toContain( OVERLAY_TEXT ); |
| 202 | + |
| 203 | + const withOverlay = await readDelivery( page ); |
| 204 | + for ( const value of [ |
| 205 | + withOverlay.transformations, |
| 206 | + withOverlay.src, |
| 207 | + ] ) { |
| 208 | + expect( value ).toContain( `l_text:Arial_20:${ OVERLAY_TEXT }` ); |
| 209 | + expect( value ).toContain( 'fl_layer_apply' ); |
| 210 | + } |
| 211 | + |
| 212 | + // "Reset All" only clears the form; saving persists the empty overlay. |
| 213 | + await openEditAsset( page ); |
| 214 | + await expect( assetField( page, 'text_overlay_text' ) ).toHaveValue( |
| 215 | + OVERLAY_TEXT |
| 216 | + ); |
| 217 | + await page.locator( '#cld-asset-remove-text-overlay' ).click(); |
| 218 | + await expect( assetField( page, 'text_overlay_text' ) ).toHaveValue( |
| 219 | + '' |
| 220 | + ); |
| 221 | + const cleared = await saveAsset( |
| 222 | + page, |
| 223 | + page.locator( '#cld-asset-save-text-overlay' ) |
| 224 | + ); |
| 225 | + expect( cleared.text_overlay ).toBe( '' ); |
| 226 | + |
| 227 | + expect( await readDelivery( page ) ).toEqual( baseline ); |
| 228 | + } ); |
| 229 | + |
| 230 | + test( 'crop updates the delivery URL and reverts when removed', async ( { |
| 231 | + page, |
| 232 | + } ) => { |
| 233 | + const baseline = await readDelivery( page ); |
| 234 | + expect( baseline.src ).not.toContain( CROP ); |
| 235 | + |
| 236 | + await openEditAsset( page ); |
| 237 | + const input = assetField( page, 'transformations' ); |
| 238 | + await input.fill( CROP ); |
| 239 | + const saved = await saveAsset( |
| 240 | + page, |
| 241 | + page.locator( '#cld-asset-edit-save' ) |
| 242 | + ); |
| 243 | + expect( saved.transformations ).toBe( CROP ); |
| 244 | + |
| 245 | + const withCrop = await readDelivery( page ); |
| 246 | + expect( withCrop.transformations ).toContain( CROP ); |
| 247 | + expect( withCrop.src ).toContain( CROP ); |
| 248 | + |
| 249 | + await openEditAsset( page ); |
| 250 | + await expect( input ).toHaveValue( CROP ); |
| 251 | + await input.fill( '' ); |
| 252 | + const cleared = await saveAsset( |
| 253 | + page, |
| 254 | + page.locator( '#cld-asset-edit-save' ) |
| 255 | + ); |
| 256 | + expect( cleared.transformations ).toBe( '' ); |
| 257 | + |
| 258 | + expect( await readDelivery( page ) ).toEqual( baseline ); |
| 259 | + } ); |
| 260 | +} ); |
0 commit comments