From 65d6116b36a072b57823d9dfb987a06e7e20ebe1 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Tue, 27 Oct 2026 12:35:49 +0530 Subject: [PATCH 1/4] fix(e2e): Parse srcset with commas in Cloudinary URLs Cloudinary transformation segments such as w_800,h_600 contain commas, so splitting srcset on every comma broke each candidate into pieces. Parse candidates by their trailing descriptor instead and expose the descriptor through parseSrcset(). --- tests/e2e/utils/delivery.js | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/e2e/utils/delivery.js b/tests/e2e/utils/delivery.js index 7f82c3bf0..0616278ed 100644 --- a/tests/e2e/utils/delivery.js +++ b/tests/e2e/utils/delivery.js @@ -28,22 +28,45 @@ function expectCloudinaryUrl( rawUrl, expectedCloud ) { } /** - * Split a `srcset` attribute into its candidate URLs. + * Parse a `srcset` attribute into its candidates. + * + * Cloudinary URLs carry commas inside the transformation segment + * (`w_800,h_600,c_scale`), so splitting on `,` breaks them apart. A + * candidate ends at its descriptor, or at a comma followed by whitespace + * when it has none. * * @param {string|null} srcset The raw attribute value. - * @return {string[]} Candidate URLs, without width/density descriptors. + * @return {{ url: string, descriptor: string }[]} Candidates in source order. */ -function srcsetUrls( srcset ) { +function parseSrcset( srcset ) { if ( ! srcset ) { return []; } - return srcset - .split( ',' ) - .map( ( candidate ) => candidate.trim().split( /\s+/ )[ 0 ] ) - .filter( Boolean ); + const candidates = []; + const pattern = /\s*(\S+?)(?:\s+(\d+(?:\.\d+)?[wx]))?\s*(?:,(?=\s)|,?$)/gy; + let match; + while ( pattern.lastIndex < srcset.length ) { + match = pattern.exec( srcset ); + if ( ! match || ! match[ 0 ] ) { + break; + } + candidates.push( { url: match[ 1 ], descriptor: match[ 2 ] || '' } ); + } + return candidates; +} + +/** + * Split a `srcset` attribute into its candidate URLs. + * + * @param {string|null} srcset The raw attribute value. + * @return {string[]} Candidate URLs, without width/density descriptors. + */ +function srcsetUrls( srcset ) { + return parseSrcset( srcset ).map( ( candidate ) => candidate.url ); } module.exports = { expectCloudinaryUrl, + parseSrcset, srcsetUrls, }; From 107b3d359c03b707c809b5c9c13746e94ca8eda7 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Tue, 27 Oct 2026 12:35:51 +0530 Subject: [PATCH 2/4] test(e2e): Add breakpoints and lazy load spec Cover the responsive breakpoints and lazy load delivery settings on a synced image: - min/max/step produce the matching srcset width entries - lazy load renders the SVG placeholder, data attributes and the CLDLB loader config with the blur placeholder - turning either setting off removes its markup --- .../cloudinary-breakpoints-lazy-load.spec.js | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 tests/e2e/cloudinary-breakpoints-lazy-load.spec.js diff --git a/tests/e2e/cloudinary-breakpoints-lazy-load.spec.js b/tests/e2e/cloudinary-breakpoints-lazy-load.spec.js new file mode 100644 index 000000000..70843277b --- /dev/null +++ b/tests/e2e/cloudinary-breakpoints-lazy-load.spec.js @@ -0,0 +1,389 @@ +/** + * External dependencies + */ +const fs = require( 'fs' ); +const path = require( 'path' ); +const { test, expect } = require( './fixtures' ); + +/** + * Internal dependencies + */ +const { ensureCloudinaryConnected } = require( './utils/connection' ); +const { + expectCloudinaryUrl, + parseSrcset, + srcsetUrls, +} = require( './utils/delivery' ); +const { wpCli, wpEvalFile } = require( './utils/wizard' ); + +const FIXTURE_PATH = path.join( __dirname, 'fixtures', 'test-image.jpg' ); + +/** + * Intrinsic width of the fixture image. + * + * @type {number} + */ +const FIXTURE_WIDTH = 1200; + +/** + * Breakpoint settings that give widths core WordPress never produces for a + * 1200x900 image (its sizes are 1200, 1024, 768 and 300). A width from this + * set in `srcset` can only come from the plugin's breakpoints. + * + * @type {{ max_width: number, min_width: number, pixel_step: number }} + */ +const BREAKPOINTS = { + max_width: 1100, + min_width: 500, + pixel_step: 300, +}; + +/** + * Widths `Responsive_Breakpoints::apply_breakpoints()` builds for + * BREAKPOINTS: the image's own width, then max down to min in steps. + * + * @type {number[]} + */ +const EXPECTED_WIDTHS = [ FIXTURE_WIDTH, 1100, 800, 500 ]; + +/** + * Every setting the spec writes. Pinned in each test so the result does not + * depend on the order the tests run in or on state left by other specs. + * + * @type {string[]} + */ +const MANAGED_SETTINGS = [ + 'enable_breakpoints', + 'use_lazy_load', + 'lazy_placeholder', + 'max_width', + 'min_width', + 'pixel_step', +]; + +let cloudName; + +/** @type {Record|null} */ +let previousSettings = null; + +/** + * Objects created in beforeAll and removed in afterAll. + * + * @type {{ postId: number, attachmentId: number, postLink: string }|null} + */ +let created = null; + +/** + * Save plugin settings through the settings API and return the old values. + * + * @param {Record} values Setting slug to value. + * @return {Record} Setting slug to the value before the write. + */ +function saveSettings( values ) { + return JSON.parse( + wpEvalFile( ` + $settings = get_plugin_instance()->settings; + $previous = array(); + foreach ( json_decode( '${ JSON.stringify( + values + ) }', true ) as $slug => $value ) { + $setting = $settings->get_setting( $slug ); + $previous[ $slug ] = $setting->get_value(); + $setting->save_value( $value ); + } + echo wp_json_encode( $previous ); + ` ) + ); +} + +/** + * Read the current value of the managed settings. + * + * @return {Record} Setting slug to value. + */ +function readSettings() { + return JSON.parse( + wpEvalFile( ` + $settings = get_plugin_instance()->settings; + $values = array(); + foreach ( json_decode( '${ JSON.stringify( + MANAGED_SETTINGS + ) }', true ) as $slug ) { + $values[ $slug ] = $settings->get_setting( $slug )->get_value(); + } + echo wp_json_encode( $values ); + ` ) + ); +} + +/** + * Apply the breakpoint and lazy-load toggles, with the fixed breakpoint + * range and the blur placeholder. + * + * @param {Object} options + * @param {boolean} options.breakpoints Enable responsive breakpoints. + * @param {boolean} options.lazyLoad Enable lazy loading. + */ +function configure( { breakpoints, lazyLoad } ) { + saveSettings( { + ...BREAKPOINTS, + enable_breakpoints: breakpoints ? 'on' : 'off', + use_lazy_load: lazyLoad ? 'on' : 'off', + lazy_placeholder: 'blur', + } ); +} + +/** + * Parse the width descriptors out of a `srcset` attribute. + * + * @param {string|null} srcset The raw attribute value. + * @return {number[]} Widths in source order. + */ +function srcsetWidths( srcset ) { + return parseSrcset( srcset ) + .map( ( candidate ) => candidate.descriptor ) + .filter( ( descriptor ) => /^\d+w$/.test( descriptor ) ) + .map( ( descriptor ) => parseInt( descriptor, 10 ) ); +} + +/** + * Load the post with JavaScript disabled and return the attributes of the + * inline image as the server rendered them. + * + * The lazy-load script rewrites `src` and `srcset` once it runs, so the + * server markup can only be read reliably without it. + * + * @param {import('@playwright/test').Browser} browser + * @param {string} baseURL + * @return {Promise<{ attrs: Record, html: string }>} Image attributes and the page HTML. + */ +async function serverMarkup( browser, baseURL ) { + const context = await browser.newContext( { + baseURL, + javaScriptEnabled: false, + } ); + try { + const page = await context.newPage(); + await page.goto( created.postLink ); + const img = page + .locator( `.wp-block-image img.wp-image-${ created.attachmentId }` ) + .first(); + await expect( + img, + 'inline image block should render in post content' + ).toBeAttached(); + + const attrs = await img.evaluate( ( el ) => + Object.fromEntries( + [ ...el.attributes ].map( ( a ) => [ a.name, a.value ] ) + ) + ); + return { attrs, html: await page.content() }; + } finally { + await context.close(); + } +} + +// @serial: needs real credentials in `cloudinary_connect` for `wp cloudinary +// sync`, while every analytics spec overwrites that option with fake ones. +test.describe( + 'Cloudinary responsive breakpoints and lazy load', + { tag: '@serial' }, + () => { + test.beforeAll( async ( { requestUtils } ) => { + ( { cloudName } = ensureCloudinaryConnected() ); + previousSettings = readSettings(); + + // A timestamped filename gives every run a fresh public ID, + // so no CDN cache from an earlier run can interfere. + const media = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/media', + headers: { + 'Content-Type': 'image/jpeg', + 'Content-Disposition': `attachment; filename="breakpoints-${ 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 breakpoints e2e ${ Date.now() }`, + content, + }, + } ); + + created = { + postId: post.id, + attachmentId: media.id, + postLink: post.link, + }; + + // Same reason as the image delivery spec: sync synchronously + // instead of waiting for the auto-sync queue. + wpCli( [ 'cloudinary', 'sync' ] ); + } ); + + test.afterAll( () => { + if ( previousSettings ) { + saveSettings( previousSettings ); + previousSettings = null; + } + 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( 'configured min/max/step produce matching srcset widths', async ( { + browser, + baseURL, + } ) => { + // Breakpoints are only written to `srcset` on the server when + // lazy loading is off; with lazy load on, the script picks + // the size at runtime instead. + configure( { breakpoints: true, lazyLoad: false } ); + + const { attrs } = await serverMarkup( browser, baseURL ); + + expect( + attrs[ 'data-responsive' ], + 'breakpoints should mark the image as responsive' + ).toBeDefined(); + expect( srcsetWidths( attrs.srcset ) ).toEqual( EXPECTED_WIDTHS ); + + const urls = srcsetUrls( attrs.srcset ); + expect( urls ).toHaveLength( EXPECTED_WIDTHS.length ); + for ( const [ i, url ] of urls.entries() ) { + expectCloudinaryUrl( url, cloudName ); + // The first candidate is the original `src`; the rest + // carry the breakpoint width as a transformation. + if ( i > 0 ) { + expect( url, `transformation of ${ url }` ).toContain( + `w_${ EXPECTED_WIDTHS[ i ] }` + ); + } + } + + expect( attrs.sizes ).toBe( + `(max-width: ${ FIXTURE_WIDTH }px) 100vw, ${ FIXTURE_WIDTH }px` + ); + } ); + + test( 'disabling breakpoints removes the breakpoint srcset', async ( { + browser, + baseURL, + } ) => { + configure( { breakpoints: false, lazyLoad: false } ); + + const { attrs } = await serverMarkup( browser, baseURL ); + + expect( + attrs[ 'data-responsive' ], + 'the responsive marker should be gone' + ).toBeUndefined(); + + // Core may still emit its own srcset from the registered + // image sizes. None of the breakpoint-only widths may appear. + const widths = srcsetWidths( attrs.srcset ); + for ( const width of EXPECTED_WIDTHS.slice( 1 ) ) { + expect( + widths, + `srcset should not contain breakpoint width ${ width }w` + ).not.toContain( width ); + } + } ); + + test( 'lazy load renders the placeholder markup', async ( { + browser, + baseURL, + page, + } ) => { + configure( { breakpoints: true, lazyLoad: true } ); + + const { attrs, html } = await serverMarkup( browser, baseURL ); + + expect( attrs[ 'data-cloudinary' ] ).toBe( 'lazy' ); + expect( + attrs.src, + 'src should be the inline SVG placeholder' + ).toMatch( /^data:image\/svg\+xml/ ); + expect( attrs[ 'data-size' ] ).toMatch( /^\d+ \d+$/ ); + expect( attrs.onload ).toContain( 'CLDBind' ); + expect( attrs[ 'data-public-id' ] ).toBeTruthy(); + expect( attrs[ 'data-delivery' ] ).toBe( 'upload' ); + + // The breakpoint srcset is not rendered on the server; the + // script sizes the image at runtime from data-responsive. + expect( attrs.srcset ).toBeUndefined(); + expect( attrs[ 'data-responsive' ] ).toBeDefined(); + + // The inline loader config carries the blur placeholder + // transformation. + const config = /var CLDLB = (\{.*?\});/.exec( html ); + expect( + config, + 'the CLDLB loader config should be printed' + ).not.toBeNull(); + const parsed = JSON.parse( config[ 1 ] ); + expect( parsed.use_lazy_load ).toBe( 'on' ); + expect( parsed.lazy_placeholder ).toBe( 'blur' ); + expect( parsed.placeholder ).toContain( 'e_blur:2000' ); + + // With JavaScript on, the script swaps in the Cloudinary URL + // once the image is in view. + await page.goto( created.postLink ); + const img = page + .locator( + `.wp-block-image img.wp-image-${ created.attachmentId }` + ) + .first(); + await img.scrollIntoViewIfNeeded(); + await expect( img ).toHaveAttribute( + 'src', + /^https:\/\/res\.cloudinary\.com\// + ); + expectCloudinaryUrl( await img.getAttribute( 'src' ), cloudName ); + } ); + + test( 'disabling lazy load removes the placeholder markup', async ( { + browser, + baseURL, + } ) => { + configure( { breakpoints: false, lazyLoad: false } ); + + const { attrs, html } = await serverMarkup( browser, baseURL ); + + expect( attrs[ 'data-cloudinary' ] ).toBeUndefined(); + expect( attrs[ 'data-size' ] ).toBeUndefined(); + expect( attrs.onload || '' ).not.toContain( 'CLDBind' ); + expect( attrs[ 'data-srcset' ] ).toBeUndefined(); + expect( html ).not.toContain( 'var CLDLB' ); + + // The real Cloudinary URL is in `src` straight from the server. + expectCloudinaryUrl( attrs.src, cloudName ); + } ); + } +); From 63da4198215969a0eca0a4ebaa9521e4b3bc25a4 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Mon, 2 Nov 2026 19:48:11 +0530 Subject: [PATCH 3/4] fix(e2e): Parse srcset with the HTML parsing rule The regex only ended a candidate at a comma followed by whitespace or at the end of the string, and quietly returned what it had so far when it did not match. Valid input such as "a.jpg 1x,b.jpg 2x" gave an empty list, which made srcset checks pass without checking anything. Read a URL as a run of non-whitespace characters, as the HTML spec does, so commas inside Cloudinary URLs still stay part of the URL. --- tests/e2e/utils/delivery.js | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/e2e/utils/delivery.js b/tests/e2e/utils/delivery.js index 0616278ed..db53d43c8 100644 --- a/tests/e2e/utils/delivery.js +++ b/tests/e2e/utils/delivery.js @@ -30,27 +30,35 @@ function expectCloudinaryUrl( rawUrl, expectedCloud ) { /** * Parse a `srcset` attribute into its candidates. * - * Cloudinary URLs carry commas inside the transformation segment - * (`w_800,h_600,c_scale`), so splitting on `,` breaks them apart. A - * candidate ends at its descriptor, or at a comma followed by whitespace - * when it has none. + * Follows the HTML parsing rule: a URL is a run of non-whitespace + * characters, and trailing commas on it end the candidate. Otherwise the + * descriptor runs up to the next comma. Commas inside a URL, such as in a + * Cloudinary transformation segment (`w_800,h_600,c_scale`), stay part of + * the URL. * * @param {string|null} srcset The raw attribute value. * @return {{ url: string, descriptor: string }[]} Candidates in source order. */ function parseSrcset( srcset ) { - if ( ! srcset ) { - return []; - } const candidates = []; - const pattern = /\s*(\S+?)(?:\s+(\d+(?:\.\d+)?[wx]))?\s*(?:,(?=\s)|,?$)/gy; - let match; - while ( pattern.lastIndex < srcset.length ) { - match = pattern.exec( srcset ); - if ( ! match || ! match[ 0 ] ) { - break; + const input = srcset || ''; + const urlPattern = /[\s,]*(\S+)/y; + const descriptorPattern = /\s*([^,]*?)\s*(?:,|$)/y; + for ( + let urlMatch = urlPattern.exec( input ); + urlMatch; + urlMatch = urlPattern.exec( input ) + ) { + let url = urlMatch[ 1 ]; + let descriptor = ''; + if ( url.endsWith( ',' ) ) { + url = url.replace( /,+$/, '' ); + } else { + descriptorPattern.lastIndex = urlPattern.lastIndex; + descriptor = descriptorPattern.exec( input )[ 1 ]; + urlPattern.lastIndex = descriptorPattern.lastIndex; } - candidates.push( { url: match[ 1 ], descriptor: match[ 2 ] || '' } ); + candidates.push( { url, descriptor } ); } return candidates; } From c90d1d636e031948aced12f0b9be22f111f439c7 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Mon, 2 Nov 2026 19:48:13 +0530 Subject: [PATCH 4/4] test(e2e): Share settings helpers and tighten checks - Move saveSettings() and readSettings() to tests/e2e/utils/settings.js and base64-encode the JSON, so a quote in a value cannot break the PHP string. - Match the breakpoint w_ parameter exactly. - Check that every width left after breakpoints are off is a registered image size. - Stop the CLDLB regex at the end of the line. - Explain why the lazy load test also turns breakpoints off. --- .../cloudinary-breakpoints-lazy-load.spec.js | 90 +++++++++---------- .../cloudinary-global-transformations.spec.js | 41 +-------- tests/e2e/cloudinary-svg-delivery.spec.js | 27 +----- tests/e2e/utils/settings.js | 67 ++++++++++++++ 4 files changed, 112 insertions(+), 113 deletions(-) create mode 100644 tests/e2e/utils/settings.js diff --git a/tests/e2e/cloudinary-breakpoints-lazy-load.spec.js b/tests/e2e/cloudinary-breakpoints-lazy-load.spec.js index 70843277b..364e920ef 100644 --- a/tests/e2e/cloudinary-breakpoints-lazy-load.spec.js +++ b/tests/e2e/cloudinary-breakpoints-lazy-load.spec.js @@ -14,6 +14,7 @@ const { parseSrcset, srcsetUrls, } = require( './utils/delivery' ); +const { readSettings, saveSettings } = require( './utils/settings' ); const { wpCli, wpEvalFile } = require( './utils/wizard' ); const FIXTURE_PATH = path.join( __dirname, 'fixtures', 'test-image.jpg' ); @@ -73,49 +74,6 @@ let previousSettings = null; */ let created = null; -/** - * Save plugin settings through the settings API and return the old values. - * - * @param {Record} values Setting slug to value. - * @return {Record} Setting slug to the value before the write. - */ -function saveSettings( values ) { - return JSON.parse( - wpEvalFile( ` - $settings = get_plugin_instance()->settings; - $previous = array(); - foreach ( json_decode( '${ JSON.stringify( - values - ) }', true ) as $slug => $value ) { - $setting = $settings->get_setting( $slug ); - $previous[ $slug ] = $setting->get_value(); - $setting->save_value( $value ); - } - echo wp_json_encode( $previous ); - ` ) - ); -} - -/** - * Read the current value of the managed settings. - * - * @return {Record} Setting slug to value. - */ -function readSettings() { - return JSON.parse( - wpEvalFile( ` - $settings = get_plugin_instance()->settings; - $values = array(); - foreach ( json_decode( '${ JSON.stringify( - MANAGED_SETTINGS - ) }', true ) as $slug ) { - $values[ $slug ] = $settings->get_setting( $slug )->get_value(); - } - echo wp_json_encode( $values ); - ` ) - ); -} - /** * Apply the breakpoint and lazy-load toggles, with the fixed breakpoint * range and the blur placeholder. @@ -146,6 +104,26 @@ function srcsetWidths( srcset ) { .map( ( descriptor ) => parseInt( descriptor, 10 ) ); } +/** + * Widths of the image sizes WordPress registered for an attachment, + * including the full size. + * + * @param {number} attachmentId Attachment ID. + * @return {number[]} Widths in pixels. + */ +function attachmentWidths( attachmentId ) { + return JSON.parse( + wpEvalFile( ` + $meta = wp_get_attachment_metadata( ${ Number( attachmentId ) } ); + $widths = array( (int) $meta['width'] ); + foreach ( $meta['sizes'] as $size ) { + $widths[] = (int) $size['width']; + } + echo wp_json_encode( $widths ); + ` ) + ); +} + /** * Load the post with JavaScript disabled and return the attributes of the * inline image as the server rendered them. @@ -192,7 +170,7 @@ test.describe( () => { test.beforeAll( async ( { requestUtils } ) => { ( { cloudName } = ensureCloudinaryConnected() ); - previousSettings = readSettings(); + previousSettings = readSettings( MANAGED_SETTINGS ); // A timestamped filename gives every run a fresh public ID, // so no CDN cache from an earlier run can interfere. @@ -281,8 +259,8 @@ test.describe( // The first candidate is the original `src`; the rest // carry the breakpoint width as a transformation. if ( i > 0 ) { - expect( url, `transformation of ${ url }` ).toContain( - `w_${ EXPECTED_WIDTHS[ i ] }` + expect( url, `transformation of ${ url }` ).toMatch( + new RegExp( `[/,]w_${ EXPECTED_WIDTHS[ i ] }[,/]` ) ); } } @@ -305,9 +283,21 @@ test.describe( 'the responsive marker should be gone' ).toBeUndefined(); - // Core may still emit its own srcset from the registered - // image sizes. None of the breakpoint-only widths may appear. + // WordPress still writes its own srcset from the registered + // image sizes. Every width must be one of those sizes, so none + // of the breakpoint-only widths can appear. const widths = srcsetWidths( attrs.srcset ); + expect( + widths.length, + 'WordPress should still write its own srcset' + ).toBeGreaterThan( 0 ); + const sizeWidths = attachmentWidths( created.attachmentId ); + for ( const width of widths ) { + expect( + sizeWidths, + `srcset width ${ width }w should be a registered image size` + ).toContain( width ); + } for ( const width of EXPECTED_WIDTHS.slice( 1 ) ) { expect( widths, @@ -342,7 +332,7 @@ test.describe( // The inline loader config carries the blur placeholder // transformation. - const config = /var CLDLB = (\{.*?\});/.exec( html ); + const config = /var CLDLB = (\{[^\n]*?\});/.exec( html ); expect( config, 'the CLDLB loader config should be printed' @@ -372,6 +362,8 @@ test.describe( browser, baseURL, } ) => { + // Breakpoints are off too, so `src` is the plain delivery URL + // rather than one the breakpoints feature sized. configure( { breakpoints: false, lazyLoad: false } ); const { attrs, html } = await serverMarkup( browser, baseURL ); diff --git a/tests/e2e/cloudinary-global-transformations.spec.js b/tests/e2e/cloudinary-global-transformations.spec.js index 705a14e9a..6cf8caf8e 100644 --- a/tests/e2e/cloudinary-global-transformations.spec.js +++ b/tests/e2e/cloudinary-global-transformations.spec.js @@ -10,7 +10,8 @@ const { test, expect } = require( './fixtures' ); */ const { ensureCloudinaryConnected } = require( './utils/connection' ); const { expectCloudinaryUrl } = require( './utils/delivery' ); -const { wpCli, wpEvalFile } = require( './utils/wizard' ); +const { readSettings, saveSettings } = require( './utils/settings' ); +const { wpCli } = require( './utils/wizard' ); const FIXTURE_PATH = path.join( __dirname, 'fixtures', 'test-image.jpg' ); @@ -59,42 +60,6 @@ let created = null; */ let createdIds = []; -/** - * Save plugin settings through the settings API. - * - * @param {Record} values Setting slug to value. - */ -function saveSettings( values ) { - wpEvalFile( ` - $settings = get_plugin_instance()->settings; - foreach ( json_decode( '${ JSON.stringify( - values - ) }', true ) as $slug => $value ) { - $settings->get_setting( $slug )->save_value( $value ); - } - ` ); -} - -/** - * Read the current value of the managed settings. - * - * @return {Record} Setting slug to value. - */ -function readSettings() { - return JSON.parse( - wpEvalFile( ` - $settings = get_plugin_instance()->settings; - $values = array(); - foreach ( json_decode( '${ JSON.stringify( - MANAGED_SETTINGS - ) }', true ) as $slug ) { - $values[ $slug ] = $settings->get_setting( $slug )->get_value(); - } - echo wp_json_encode( $values ); - ` ) - ); -} - /** * Upload the fixture, embed it in a published post and sync it. * @@ -214,7 +179,7 @@ test.describe( () => { test.beforeAll( async ( { requestUtils } ) => { ( { cloudName } = ensureCloudinaryConnected() ); - previousSettings = readSettings(); + previousSettings = readSettings( MANAGED_SETTINGS ); saveSettings( BASE_SETTINGS ); created = await uploadSyncedImagePost( requestUtils ); } ); diff --git a/tests/e2e/cloudinary-svg-delivery.spec.js b/tests/e2e/cloudinary-svg-delivery.spec.js index 7d86990ef..40df62a7a 100644 --- a/tests/e2e/cloudinary-svg-delivery.spec.js +++ b/tests/e2e/cloudinary-svg-delivery.spec.js @@ -10,6 +10,7 @@ const { test, expect } = require( './fixtures' ); */ const { ensureCloudinaryConnected } = require( './utils/connection' ); const { expectCloudinaryUrl } = require( './utils/delivery' ); +const { saveSettings } = require( './utils/settings' ); const { wpCli, wpEvalFile } = require( './utils/wizard' ); const VALID_FIXTURE = path.join( __dirname, 'fixtures', 'svg-valid.svg' ); @@ -44,32 +45,6 @@ let previousSettings = null; */ let createdIds = []; -/** - * Save plugin settings and return their previous values. - * - * Goes through the settings API rather than `wp option patch` so the values - * land in the same place the settings screen saves them. - * - * @param {Record} values Setting slug to value. - * @return {Record} Setting slug to the value before the write. - */ -function saveSettings( values ) { - return JSON.parse( - wpEvalFile( ` - $settings = get_plugin_instance()->settings; - $previous = array(); - foreach ( json_decode( '${ JSON.stringify( - values - ) }', true ) as $slug => $value ) { - $setting = $settings->get_setting( $slug ); - $previous[ $slug ] = $setting->get_value(); - $setting->save_value( $value ); - } - echo wp_json_encode( $previous ); - ` ) - ); -} - /** * Upload an SVG fixture, embed it in a published post and sync it. * diff --git a/tests/e2e/utils/settings.js b/tests/e2e/utils/settings.js new file mode 100644 index 000000000..3c6720d29 --- /dev/null +++ b/tests/e2e/utils/settings.js @@ -0,0 +1,67 @@ +/** + * Read and write plugin settings from e2e specs. + * + * Goes through the plugin's settings API rather than `wp option patch`, so + * the values land in the same place the settings screen saves them. + */ + +const { wpEvalFile } = require( './wizard' ); + +/** + * Encode a value as a PHP expression that decodes it back. + * + * The JSON is base64-encoded, so quotes and backslashes in the values + * cannot break out of the PHP string. + * + * @param {unknown} value Any JSON-serializable value. + * @return {string} PHP expression. + */ +function phpJson( value ) { + const encoded = Buffer.from( JSON.stringify( value ) ).toString( 'base64' ); + return `json_decode( base64_decode( '${ encoded }' ), true )`; +} + +/** + * Save plugin settings and return their previous values. + * + * @param {Record} values Setting slug to value. + * @return {Record} Setting slug to the value before the write. + */ +function saveSettings( values ) { + return JSON.parse( + wpEvalFile( ` + $settings = get_plugin_instance()->settings; + $previous = array(); + foreach ( ${ phpJson( values ) } as $slug => $value ) { + $setting = $settings->get_setting( $slug ); + $previous[ $slug ] = $setting->get_value(); + $setting->save_value( $value ); + } + echo wp_json_encode( $previous ); + ` ) + ); +} + +/** + * Read the current value of plugin settings. + * + * @param {string[]} slugs Setting slugs. + * @return {Record} Setting slug to value. + */ +function readSettings( slugs ) { + return JSON.parse( + wpEvalFile( ` + $settings = get_plugin_instance()->settings; + $values = array(); + foreach ( ${ phpJson( slugs ) } as $slug ) { + $values[ $slug ] = $settings->get_setting( $slug )->get_value(); + } + echo wp_json_encode( $values ); + ` ) + ); +} + +module.exports = { + readSettings, + saveSettings, +};