| @@ -10,8 +10,97 @@ | ||
| 10 | 10 | const SD_API = '/suredonation/v1'; |
| 11 | 11 | const CAMPAIGN_CPT = 'suredonation_cmpgn'; |
| 12 | 12 | |
| 13 | 13 | /** |
| 14 | + * Unwrap a settings REST response. | |
| 15 | + * | |
| 16 | + * Endpoints return either `{ success, settings: {...} }` or the settings object | |
| 17 | + * directly; this centralizes that shape assumption so it lives in one place. | |
| 18 | + * | |
| 19 | + * @param {Object} response REST response. | |
| 20 | + * @return {Object} The settings object. | |
| 21 | + */ | |
| 22 | +function unwrapSettings( response ) { | |
| 23 | + return ( response && response.settings ) || response || {}; | |
| 24 | +} | |
| 25 | + | |
| 26 | +let uniqueEmailCounter = 0; | |
| 27 | + | |
| 28 | +/** | |
| 29 | + * Generate a collision-free test email. | |
| 30 | + * | |
| 31 | + * `Date.now()` alone can repeat within the same millisecond across fast | |
| 32 | + * sequential submits, so a monotonic counter is appended. | |
| 33 | + * | |
| 34 | + * @param {string} prefix Email local-part prefix. | |
| 35 | + * @return {string} Unique `@test.local` email address. | |
| 36 | + */ | |
| 37 | +function uniqueEmail( prefix = 'e2e' ) { | |
| 38 | + uniqueEmailCounter += 1; | |
| 39 | + return `${ prefix }-${ Date.now() }-${ uniqueEmailCounter }@test.local`; | |
| 40 | +} | |
| 41 | + | |
| 42 | +/** | |
| 43 | + * Get current spam-protection (honeypot) settings via REST API. | |
| 44 | + * | |
| 45 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 46 | + * @return {Promise<Object>} API response: { success, settings: { honeypot } }. | |
| 47 | + */ | |
| 48 | +async function getSpamProtectionSettings( requestUtils ) { | |
| 49 | + return requestUtils.rest( { | |
| 50 | + method: 'GET', | |
| 51 | + path: `${ SD_API }/settings/spam-protection`, | |
| 52 | + } ); | |
| 53 | +} | |
| 54 | + | |
| 55 | +/** | |
| 56 | + * Toggle the honeypot setting via REST API. | |
| 57 | + * | |
| 58 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 59 | + * @param {boolean} enabled Whether the honeypot should be enabled. | |
| 60 | + * @return {Promise<Object>} API response. | |
| 61 | + */ | |
| 62 | +async function setHoneypot( requestUtils, enabled ) { | |
| 63 | + try { | |
| 64 | + return await requestUtils.rest( { | |
| 65 | + method: 'POST', | |
| 66 | + path: `${ SD_API }/settings/spam-protection`, | |
| 67 | + data: { honeypot: enabled }, | |
| 68 | + } ); | |
| 69 | + } catch ( e ) { | |
| 70 | + // Defensive fallback (mirrors enableOfflineDonations): if the option | |
| 71 | + // write reports unchanged and the API surfaces an error, verify via GET. | |
| 72 | + const settings = unwrapSettings( | |
| 73 | + await getSpamProtectionSettings( requestUtils ) | |
| 74 | + ); | |
| 75 | + if ( settings.honeypot === enabled ) { | |
| 76 | + return { success: true, settings }; | |
| 77 | + } | |
| 78 | + throw e; | |
| 79 | + } | |
| 80 | +} | |
| 81 | + | |
| 82 | +/** | |
| 83 | + * Enable the honeypot spam protection via REST API. | |
| 84 | + * | |
| 85 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 86 | + * @return {Promise<Object>} API response. | |
| 87 | + */ | |
| 88 | +async function enableHoneypot( requestUtils ) { | |
| 89 | + return setHoneypot( requestUtils, true ); | |
| 90 | +} | |
| 91 | + | |
| 92 | +/** | |
| 93 | + * Disable the honeypot spam protection via REST API. | |
| 94 | + * | |
| 95 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 96 | + * @return {Promise<Object>} API response. | |
| 97 | + */ | |
| 98 | +async function disableHoneypot( requestUtils ) { | |
| 99 | + return setHoneypot( requestUtils, false ); | |
| 100 | +} | |
| 101 | + | |
| 102 | +/** | |
| 14 | 103 | * Create a test campaign via the WP REST API. |
| 15 | 104 | * |
| 16 | 105 | * @param {Object} requestUtils Playwright requestUtils fixture. |
| 17 | 106 | * @return {Promise<number>} Campaign post ID. |
| @@ -414,9 +503,9 @@ | ||
| 414 | 503 | // Inject block_id (and formId for payment block) into ALL suredonation/* blocks. |
| 415 | 504 | // The Gutenberg editor normally sets block_id on mount via useEffect. |
| 416 | 505 | // Server-side validation skips blocks without block_id, so we must inject them. |
| 417 | 506 | updatedContent = updatedContent.replace( |
| 418 | - /<!-- wp:sd\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 507 | + /<!-- wp:suredonation\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 419 | 508 | ( match, blockName ) => { |
| 420 | 509 | const id = Math.random().toString( 36 ).substring( 2, 9 ); |
| 421 | 510 | if ( blockName === 'payment' ) { |
| 422 | 511 | return `<!-- wp:suredonation/${ blockName } {"block_id":"${ id }","formId":${ formId },`; |
| @@ -566,8 +655,47 @@ | ||
| 566 | 655 | } |
| 567 | 656 | } |
| 568 | 657 | |
| 569 | 658 | /** |
| 659 | + * Get the global payment settings via the Stripe settings REST API. | |
| 660 | + * | |
| 661 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 662 | + * @return {Promise<Object>} Payment settings object (currency, payment_mode, …). | |
| 663 | + */ | |
| 664 | +async function getPaymentSettings( requestUtils ) { | |
| 665 | + const response = await requestUtils.rest( { | |
| 666 | + method: 'GET', | |
| 667 | + path: `${ SD_API }/payments/stripe/settings`, | |
| 668 | + } ); | |
| 669 | + return response.settings || response; | |
| 670 | +} | |
| 671 | + | |
| 672 | +/** | |
| 673 | + * Set the global payment mode (test|live) via the Stripe settings REST API. | |
| 674 | + * | |
| 675 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 676 | + * @param {string} mode 'test' or 'live'. | |
| 677 | + * @return {Promise<Object>} API response. | |
| 678 | + */ | |
| 679 | +async function setPaymentMode( requestUtils, mode ) { | |
| 680 | + try { | |
| 681 | + return await requestUtils.rest( { | |
| 682 | + method: 'POST', | |
| 683 | + path: `${ SD_API }/payments/stripe/settings`, | |
| 684 | + data: { payment_mode: mode }, | |
| 685 | + } ); | |
| 686 | + } catch ( e ) { | |
| 687 | + // WordPress update_option returns false when the value is unchanged, | |
| 688 | + // surfacing as an API error. Verify via GET instead. | |
| 689 | + const current = await getPaymentSettings( requestUtils ); | |
| 690 | + if ( current.payment_mode === mode ) { | |
| 691 | + return { success: true, settings: current }; | |
| 692 | + } | |
| 693 | + throw e; | |
| 694 | + } | |
| 695 | +} | |
| 696 | + | |
| 697 | +/** | |
| 570 | 698 | * Get fee recovery settings via the Stripe settings REST API. |
| 571 | 699 | * |
| 572 | 700 | * @param {Object} requestUtils Playwright requestUtils fixture. |
| 573 | 701 | * @return {Promise<Object>} Fee recovery settings object. |
| @@ -668,9 +796,9 @@ | ||
| 668 | 796 | } |
| 669 | 797 | |
| 670 | 798 | // 5. Inject block_id into all suredonation/* blocks (same pattern as createOfflineFormPage). |
| 671 | 799 | updatedContent = updatedContent.replace( |
| 672 | - /<!-- wp:sd\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 800 | + /<!-- wp:suredonation\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 673 | 801 | ( match, blockName ) => { |
| 674 | 802 | const id = Math.random().toString( 36 ).substring( 2, 9 ); |
| 675 | 803 | if ( blockName === 'payment' ) { |
| 676 | 804 | return `<!-- wp:suredonation/${ blockName } {"block_id":"${ id }","formId":${ formId },`; |
| @@ -806,9 +934,9 @@ | ||
| 806 | 934 | * @return {string} Updated content with block_id injected. |
| 807 | 935 | */ |
| 808 | 936 | function injectBlockIds( content, formId ) { |
| 809 | 937 | return content.replace( |
| 810 | - /<!-- wp:sd\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 938 | + /<!-- wp:suredonation\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 811 | 939 | ( match, blockName ) => { |
| 812 | 940 | const id = Math.random().toString( 36 ).substring( 2, 9 ); |
| 813 | 941 | if ( blockName === 'payment' ) { |
| 814 | 942 | return `<!-- wp:suredonation/${ blockName } {"block_id":"${ id }","formId":${ formId },`; |
| @@ -992,9 +1120,175 @@ | ||
| 992 | 1120 | } |
| 993 | 1121 | } |
| 994 | 1122 | } |
| 995 | 1123 | |
| 1124 | +// ─── Form Validation Test Data ────────────────────────────────── | |
| 1125 | + | |
| 996 | 1126 | /** |
| 1127 | + * Get the form-validation default messages via REST API. | |
| 1128 | + * | |
| 1129 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 1130 | + * @return {Promise<Object>} API response: { success, settings: { ...messages } }. | |
| 1131 | + */ | |
| 1132 | +async function getValidationSettings( requestUtils ) { | |
| 1133 | + return requestUtils.rest( { | |
| 1134 | + method: 'GET', | |
| 1135 | + path: `${ SD_API }/settings/validation`, | |
| 1136 | + } ); | |
| 1137 | +} | |
| 1138 | + | |
| 1139 | +/** | |
| 1140 | + * Update the form-validation default messages via REST API. | |
| 1141 | + * | |
| 1142 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 1143 | + * @param {Object} messages Map of message key => message. | |
| 1144 | + * @return {Promise<Object>} API response. | |
| 1145 | + */ | |
| 1146 | +async function updateValidationSettings( requestUtils, messages ) { | |
| 1147 | + try { | |
| 1148 | + return await requestUtils.rest( { | |
| 1149 | + method: 'POST', | |
| 1150 | + path: `${ SD_API }/settings/validation`, | |
| 1151 | + data: messages, | |
| 1152 | + } ); | |
| 1153 | + } catch ( e ) { | |
| 1154 | + // WordPress update_option returns false when the value is unchanged, | |
| 1155 | + // which can surface as an API error. Verify via GET instead. | |
| 1156 | + const current = await getValidationSettings( requestUtils ); | |
| 1157 | + const settings = current.settings || current; | |
| 1158 | + const matches = Object.keys( messages ).every( | |
| 1159 | + ( key ) => settings[ key ] === messages[ key ] | |
| 1160 | + ); | |
| 1161 | + if ( matches ) { | |
| 1162 | + return { success: true, settings }; | |
| 1163 | + } | |
| 1164 | + throw e; | |
| 1165 | + } | |
| 1166 | +} | |
| 1167 | + | |
| 1168 | +/** | |
| 1169 | + * Create a donation form page for field-validation tests. | |
| 1170 | + * | |
| 1171 | + * Builds on the default form (name, email, donation-amount, payment, | |
| 1172 | + * donate-button) and optionally injects a required number field (slug | |
| 1173 | + * "quantity") with min/max bounds and/or a per-field custom Error Message on | |
| 1174 | + * the name input. block_id attributes are injected so server-side validation | |
| 1175 | + * runs. Rendered via the [suredonation_form] shortcode. | |
| 1176 | + * | |
| 1177 | + * @param {Object} requestUtils Playwright requestUtils fixture. | |
| 1178 | + * @param {Object} options Options. | |
| 1179 | + * @param {string} options.inputErrorMsg Custom Error Message for the name field. | |
| 1180 | + * @param {number} options.minValue Number field minimum (default 5). | |
| 1181 | + * @param {number} options.maxValue Number field maximum (default 50). | |
| 1182 | + * @param {boolean} options.numberField Set false to omit the number field. | |
| 1183 | + * @return {Promise<Object>} { pageUrl, campaignId, formId, pageId }. | |
| 1184 | + */ | |
| 1185 | +async function createValidationFormPage( requestUtils, options = {} ) { | |
| 1186 | + // Standalone so the form renders without a campaign dependency. | |
| 1187 | + const { campaignId, formId } = await prepareFormAndCampaign( requestUtils, { | |
| 1188 | + standalone: true, | |
| 1189 | + } ); | |
| 1190 | + | |
| 1191 | + const form = await requestUtils.rest( { | |
| 1192 | + method: 'GET', | |
| 1193 | + path: `/wp/v2/${ FORM_CPT }/${ formId }?context=edit`, | |
| 1194 | + } ); | |
| 1195 | + | |
| 1196 | + let content = form.content?.raw || ''; | |
| 1197 | + | |
| 1198 | + // Inject a block_id into every suredonation/* field block that lacks one. | |
| 1199 | + // The default template only sets slugs (the editor adds block_id on mount), | |
| 1200 | + // and server-side validation skips blocks without a block_id — so REST-built | |
| 1201 | + // forms need them injected for the stored block config to include the fields. | |
| 1202 | + content = content.replace( | |
| 1203 | + /<!-- wp:suredonation\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 1204 | + ( match, blockName ) => { | |
| 1205 | + const id = Math.random().toString( 36 ).substring( 2, 9 ); | |
| 1206 | + if ( blockName === 'payment' ) { | |
| 1207 | + return `<!-- wp:suredonation/${ blockName } {"block_id":"${ id }","formId":${ formId },`; | |
| 1208 | + } | |
| 1209 | + return `<!-- wp:suredonation/${ blockName } {"block_id":"${ id }",`; | |
| 1210 | + } | |
| 1211 | + ); | |
| 1212 | + | |
| 1213 | + // Set a per-field custom Error Message on the name (input) block. Default | |
| 1214 | + // forms omit the empty errorMsg attribute, so inject it into the opening tag. | |
| 1215 | + if ( options.inputErrorMsg ) { | |
| 1216 | + content = content.replace( | |
| 1217 | + /<!-- wp:suredonation\/input \{/, | |
| 1218 | + `<!-- wp:suredonation/input {"errorMsg":${ JSON.stringify( | |
| 1219 | + options.inputErrorMsg | |
| 1220 | + ) },` | |
| 1221 | + ); | |
| 1222 | + } | |
| 1223 | + | |
| 1224 | + // Set a per-field custom invalid-email message on the email block. It ships | |
| 1225 | + // with a non-empty default, so replace the existing value if present. | |
| 1226 | + if ( options.emailInvalidMsg ) { | |
| 1227 | + if ( /"invalidEmailMsg"\s*:/.test( content ) ) { | |
| 1228 | + content = content.replace( | |
| 1229 | + /"invalidEmailMsg"\s*:\s*"(?:[^"\\]|\\.)*"/, | |
| 1230 | + `"invalidEmailMsg":${ JSON.stringify( options.emailInvalidMsg ) }` | |
| 1231 | + ); | |
| 1232 | + } else { | |
| 1233 | + content = content.replace( | |
| 1234 | + /<!-- wp:suredonation\/email \{/, | |
| 1235 | + `<!-- wp:suredonation/email {"invalidEmailMsg":${ JSON.stringify( | |
| 1236 | + options.emailInvalidMsg | |
| 1237 | + ) },` | |
| 1238 | + ); | |
| 1239 | + } | |
| 1240 | + } | |
| 1241 | + | |
| 1242 | + // Inject a required number field with explicit slug (so update_field_slugs | |
| 1243 | + // leaves it alone) and min/max bounds, before the donate button. | |
| 1244 | + if ( options.numberField !== false ) { | |
| 1245 | + const numberAttrs = { | |
| 1246 | + block_id: Math.random().toString( 36 ).substring( 2, 9 ), | |
| 1247 | + slug: 'quantity', | |
| 1248 | + label: 'Quantity', | |
| 1249 | + required: true, | |
| 1250 | + minValue: options.minValue ?? 5, | |
| 1251 | + maxValue: options.maxValue ?? 50, | |
| 1252 | + formId, | |
| 1253 | + }; | |
| 1254 | + if ( options.numberErrorMsg ) { | |
| 1255 | + numberAttrs.errorMsg = options.numberErrorMsg; | |
| 1256 | + } | |
| 1257 | + const numberBlock = `<!-- wp:suredonation/number ${ JSON.stringify( | |
| 1258 | + numberAttrs | |
| 1259 | + ) } /-->`; | |
| 1260 | + | |
| 1261 | + if ( content.includes( '<!-- wp:suredonation/donate-button' ) ) { | |
| 1262 | + content = content.replace( | |
| 1263 | + '<!-- wp:suredonation/donate-button', | |
| 1264 | + numberBlock + '\n<!-- wp:suredonation/donate-button' | |
| 1265 | + ); | |
| 1266 | + } else { | |
| 1267 | + content += '\n' + numberBlock; | |
| 1268 | + } | |
| 1269 | + } | |
| 1270 | + | |
| 1271 | + await requestUtils.rest( { | |
| 1272 | + method: 'PUT', | |
| 1273 | + path: `/wp/v2/${ FORM_CPT }/${ formId }`, | |
| 1274 | + data: { content }, | |
| 1275 | + } ); | |
| 1276 | + | |
| 1277 | + const page = await requestUtils.rest( { | |
| 1278 | + method: 'POST', | |
| 1279 | + path: '/wp/v2/pages', | |
| 1280 | + data: { | |
| 1281 | + title: `E2E Field Validation ${ Date.now() }`, | |
| 1282 | + status: 'publish', | |
| 1283 | + content: `[suredonation_form id="${ formId }"]`, | |
| 1284 | + }, | |
| 1285 | + } ); | |
| 1286 | + | |
| 1287 | + return { pageUrl: page.link, campaignId, formId, pageId: page.id }; | |
| 1288 | +} | |
| 1289 | + | |
| 1290 | +/** | |
| 997 | 1291 | * Get PayPal settings via the plugin REST API. |
| 998 | 1292 | * |
| 999 | 1293 | * @param {Object} requestUtils Playwright requestUtils fixture. |
| 1000 | 1294 | * @return {Promise<Object>} API response with settings. |
| @@ -1183,9 +1477,9 @@ | ||
| 1183 | 1477 | } |
| 1184 | 1478 | |
| 1185 | 1479 | // Inject block_id (and formId for payment block) into ALL suredonation/* blocks. |
| 1186 | 1480 | updatedContent = updatedContent.replace( |
| 1187 | - /<!-- wp:sd\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 1481 | + /<!-- wp:suredonation\/(\w[\w-]*) \{(?!"block_id")/g, | |
| 1188 | 1482 | ( match, blockName ) => { |
| 1189 | 1483 | const id = Math.random().toString( 36 ).substring( 2, 9 ); |
| 1190 | 1484 | if ( blockName === 'payment' ) { |
| 1191 | 1485 | return `<!-- wp:suredonation/${ blockName } {"block_id":"${ id }","formId":${ formId },`; |
| @@ -1399,8 +1693,13 @@ | ||
| 1399 | 1693 | } |
| 1400 | 1694 | } |
| 1401 | 1695 | |
| 1402 | 1696 | module.exports = { |
| 1697 | + unwrapSettings, | |
| 1698 | + uniqueEmail, | |
| 1699 | + getSpamProtectionSettings, | |
| 1700 | + enableHoneypot, | |
| 1701 | + disableHoneypot, | |
| 1403 | 1702 | createTestCampaign, |
| 1404 | 1703 | createTestDonation, |
| 1405 | 1704 | seedRecurringTestData, |
| 1406 | 1705 | cleanupTestData, |
| @@ -1412,8 +1711,10 @@ | ||
| 1412 | 1711 | createOfflineFormPage, |
| 1413 | 1712 | cleanupOfflineFormPage, |
| 1414 | 1713 | seedOfflineTestData, |
| 1415 | 1714 | cleanupOfflineTestData, |
| 1715 | + getPaymentSettings, | |
| 1716 | + setPaymentMode, | |
| 1416 | 1717 | getFeeRecoverySettings, |
| 1417 | 1718 | updateFeeRecoverySettings, |
| 1418 | 1719 | createFeeRecoveryFormPage, |
| 1419 | 1720 | cleanupFeeRecoveryFormPage, |
| @@ -1429,5 +1730,8 @@ | ||
| 1429 | 1730 | createPayPalFormPage, |
| 1430 | 1731 | cleanupPayPalFormPage, |
| 1431 | 1732 | seedDonorTestData, |
| 1432 | 1733 | cleanupDonorTestData, |
| 1734 | + getValidationSettings, | |
| 1735 | + updateValidationSettings, | |
| 1736 | + createValidationFormPage, | |
| 1433 | 1737 | }; |