diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImpl.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImpl.java index 3f93b6f050..d7b53b7a6b 100644 --- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImpl.java +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImpl.java @@ -31,6 +31,8 @@ import org.apache.sling.models.annotations.injectorspecific.OSGiService; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.adobe.aemds.guide.model.ReCaptchaConfigurationModel; import com.adobe.aemds.guide.service.CloudConfigurationProvider; @@ -50,6 +52,8 @@ @Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION) public class RecaptchaImpl extends AbstractCaptchaImpl implements Captcha { + private static final Logger LOGGER = LoggerFactory.getLogger(RecaptchaImpl.class); + @Inject private ResourceResolver resourceResolver; @@ -123,4 +127,28 @@ public Map getCaptchaProperties() throws GuideException { return customCaptchaProperties; } + + @Override + public String getCaptchaDisplayMode() { + // reCAPTCHA v3 has no visible challenge (badge-only), so it is treated as invisible. This + // lets the runtime's built-in submitForm auto-fetch the token before submit, the same way + // it already does for invisible turnstile and enterprise-score keys. + CaptchaDisplayMode captchaDisplayMode = CaptchaDisplayMode.VISIBLE; + String version = null; + resource = resourceResolver.getResource(this.getPath()); + if (resource != null && cloudConfigurationProvider != null) { + try { + reCaptchaConfiguration = cloudConfigurationProvider.getRecaptchaCloudConfiguration(resource); + if (reCaptchaConfiguration != null) { + version = reCaptchaConfiguration.version(); + } + } catch (GuideException e) { + LOGGER.error("[AF] [Captcha] [RECAPTCHA] Error while fetching cloud configuration to resolve display mode.", e); + } + } + if ("v3".equals(version)) { + captchaDisplayMode = CaptchaDisplayMode.INVISIBLE; + } + return captchaDisplayMode.getValue(); + } } diff --git a/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImplTest.java b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImplTest.java index cca499db8a..a6c9e14011 100644 --- a/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImplTest.java +++ b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImplTest.java @@ -136,6 +136,31 @@ void getEnterpriseUrl() { assertEquals("https://www.recaptcha.net/recaptcha/enterprise.js", enterpriseUrl); } + @Test + void getV3Url() { + // reCAPTCHA v3 uses the classic (non-enterprise) endpoint, same as v2. + when(reCaptchaConfiguration.version()).thenReturn("v3"); + Captcha recaptcha = Utils.getComponentUnderTest(PATH_RECAPTCHA, Captcha.class, context); + Map captchaProps = recaptcha.getCaptchaProperties(); + assertEquals("https://www.recaptcha.net/recaptcha/api.js", captchaProps.get("uri")); + assertEquals("v3", captchaProps.get("version")); + } + + @Test + void getCaptchaDisplayModeForV3() { + // v3 has no visible challenge, so it must report as invisible for auto token fetch on submit. + when(reCaptchaConfiguration.version()).thenReturn("v3"); + Captcha recaptcha = Utils.getComponentUnderTest(PATH_RECAPTCHA, Captcha.class, context); + assertEquals("invisible", recaptcha.getCaptchaDisplayMode()); + } + + @Test + void getCaptchaDisplayModeForNonV3() { + when(reCaptchaConfiguration.version()).thenReturn("enterprise"); + Captcha recaptcha = Utils.getComponentUnderTest(PATH_RECAPTCHA, Captcha.class, context); + assertEquals("visible", recaptcha.getCaptchaDisplayMode()); + } + @Test void testNoFieldType() { Captcha recaptcha = Utils.getComponentUnderTest(PATH_RECAPTCHA_WITHOUT_FEILDTYPE, Captcha.class, context); diff --git a/bundles/af-core/src/test/resources/form/recaptcha/exporter-recaptcha.json b/bundles/af-core/src/test/resources/form/recaptcha/exporter-recaptcha.json index 636bd13dc3..22d6912e65 100644 --- a/bundles/af-core/src/test/resources/form/recaptcha/exporter-recaptcha.json +++ b/bundles/af-core/src/test/resources/form/recaptcha/exporter-recaptcha.json @@ -31,5 +31,6 @@ }, "customProp": "customPropValue" }, + "captchaDisplayMode": "visible", ":type": "core/fd/components/form/recaptcha/v1/recaptcha" } \ No newline at end of file diff --git a/it/config/src/main/content/jcr_root/apps/system/config/com.adobe.granite.toggle.impl.dev.DynamicToggleProviderImpl.cfg.json b/it/config/src/main/content/jcr_root/apps/system/config/com.adobe.granite.toggle.impl.dev.DynamicToggleProviderImpl.cfg.json index 95c4e89144..4b8c218bf4 100644 --- a/it/config/src/main/content/jcr_root/apps/system/config/com.adobe.granite.toggle.impl.dev.DynamicToggleProviderImpl.cfg.json +++ b/it/config/src/main/content/jcr_root/apps/system/config/com.adobe.granite.toggle.impl.dev.DynamicToggleProviderImpl.cfg.json @@ -27,7 +27,8 @@ "FT_FORMS-17107", "FT_FORMS-24087", "FT_FORMS-24343", - "FT_FORMS-25252" + "FT_FORMS-25252", + "FT_FORMS-27648" ], "disabledToggles": [ ] diff --git a/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/v3/.content.xml b/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/v3/.content.xml new file mode 100644 index 0000000000..c729bef5c7 --- /dev/null +++ b/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/v3/.content.xml @@ -0,0 +1,17 @@ + + + + diff --git a/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/recaptcha/v3/.content.xml b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/recaptcha/v3/.content.xml new file mode 100644 index 0000000000..0e8ecf4e34 --- /dev/null +++ b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/recaptcha/v3/.content.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/container/v2/container/clientlibs/site/js/formcontainerview.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/container/v2/container/clientlibs/site/js/formcontainerview.js index 8ef966825e..6ac34c622e 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/container/v2/container/clientlibs/site/js/formcontainerview.js +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/container/v2/container/clientlibs/site/js/formcontainerview.js @@ -34,7 +34,9 @@ this._model.subscribe((action) => { let state = action.target.getState(); // execute the handler only if there are no rules configured on submitSuccess event. - if (!state.events.submitSuccess || state.events.submitSuccess.length === 0) { + // getState() omits `events` in the XFA runtime, so guard the access (optional + // chaining) to avoid a TypeError; a missing events object means no rule is configured. + if (!state.events?.submitSuccess || state.events.submitSuccess.length === 0) { const globals = { form: self.getModel().getRuleNode(), event: { @@ -48,7 +50,7 @@ this._model.subscribe((action) => { let state = action.target.getState(); // execute the handler only if there are no rules configured on submitError event. - if (!state.events.submitError || state.events.submitError.length === 0) { + if (!state.events?.submitError || state.events.submitError.length === 0) { let defaultSubmissionError = FormView.LanguageUtils.getTranslatedString(self.getLang(), "InternalFormSubmissionError"); const globals = { form: self.getModel().getRuleNode(), @@ -64,7 +66,7 @@ this._model.subscribe((action) => { let state = action.target.getState(); // execute the handler only if there are no rules configured on custom:saveSuccess event. - if (!state.events['custom:saveSuccess'] || state.events['custom:saveSuccess'].length === 0) { + if (!state.events?.['custom:saveSuccess'] || state.events['custom:saveSuccess'].length === 0) { console.log("Draft id = " + action?.payload?.body?.draftId); window.alert(FormView.LanguageUtils.getTranslatedString(self.getLang(), "saveDraftSuccessMessage")); } @@ -72,7 +74,7 @@ this._model.subscribe((action) => { let state = action.target.getState(); // execute the handler only if there are no rules configured on custom:saveError event. - if (!state.events['custom:saveError'] || state.events['custom:saveError'].length === 0) { + if (!state.events?.['custom:saveError'] || state.events['custom:saveError'].length === 0) { window.alert(FormView.LanguageUtils.getTranslatedString(self.getLang(), "saveDraftErrorMessage")); } }, "saveError"); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/README.md b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/README.md index 71194988f5..eec07ce132 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/README.md +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/README.md @@ -24,6 +24,9 @@ Adaptive Form Recaptcha field component written in HTL. * Supports the following recaptcha versions: * v2 * Only visible behaviour is supported + * v3 + * Only invisible behaviour is supported (badge-only, no visible challenge); the token is + auto-fetched at submit. Requires the FT_FORMS-27648 feature toggle to be enabled. * Enterprise * Checkbox keys (Visible Behaviour) * Score-based keys (Invisible behaviour) @@ -45,7 +48,7 @@ The following properties are written to JCR for this Form Recaptcha component an 7. `./requiredMessage` - defines the message displayed as tooltip when submitting the form if the value is left empty 8. `./readOnly` - if set to `true`, the filed will be read only 9. `./rcCloudServicePath` - defines the path of cloud configuration resource for reCAPTCHA -10. `./recaptchaSize` - defines the size attribute of Google reCAPTCHA. This property is only valid for reCAPTCHA v2 and the checkbox keys of reCAPTCHA enterprise. This property will be disabled for reCAPTCHA Enteprise score-based keys. +10. `./recaptchaSize` - defines the size attribute of Google reCAPTCHA. This property is only valid for reCAPTCHA v2 and the checkbox keys of reCAPTCHA enterprise. This property will be disabled for reCAPTCHA Enterprise score-based keys and for reCAPTCHA v3 (both invisible). ## Client Libraries The component provides a `core.forms.components.recaptcha.v1.runtime` client library category that contains the Javascript runtime for the component. diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/clientlibs/editor/js/editDialog.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/clientlibs/editor/js/editDialog.js index 898a177076..90435bc98c 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/clientlibs/editor/js/editDialog.js +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/clientlibs/editor/js/editDialog.js @@ -40,7 +40,9 @@ const selectedVersion = selectedConfig.getAttribute("data-version"); const selectedKeyType = selectedConfig.getAttribute("data-key.type"); const inputs = recaptchaSize.querySelectorAll('input'); - if (selectedVersion === "enterprise" && selectedKeyType === "score") { + // v3 has no keyType concept - it is always invisible/score-based, same as + // Enterprise's score-based key case. + if ((selectedVersion === "enterprise" && selectedKeyType === "score") || selectedVersion === "v3") { inputs.forEach(input => input.setAttribute("disabled", true)); } else { inputs.forEach(input => input.removeAttribute("disabled")); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/clientlibs/site/js/recaptchawidget.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/clientlibs/site/js/recaptchawidget.js index 7e9e67e149..0811650296 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/clientlibs/site/js/recaptchawidget.js +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/recaptcha/v1/recaptcha/clientlibs/site/js/recaptchawidget.js @@ -45,11 +45,14 @@ if (typeof window.RecaptchaWidget === 'undefined') { var self = this; var recaptchaConfigData = this.#options; + // reCAPTCHA v3 has no "invisible" size setting like classic v2 - it is always + // badge-only/invisible, so it needs the same treatment as an explicit invisible size. + var isRecaptchaV3 = recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.version === "v3"; element.innerHTML = '
'; var gcontainer = document.getElementsByClassName("g-recaptcha")[0]; var widgetId; var url = recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.uri; - if (recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.size == "invisible") { + if (recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.size == "invisible" || isRecaptchaV3) { gcontainer.classList.add('g-recaptcha-invisible'); recaptchaConfigData.required = false; } @@ -94,7 +97,7 @@ if (typeof window.RecaptchaWidget === 'undefined') { var runtimeLocale = this.#lang; var scr = document.createElement('script'); - let queryParams = isScoreBasedKey() ? "?render=" + recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.siteKey + let queryParams = (isScoreBasedKey() || isRecaptchaV3) ? "?render=" + recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.siteKey : "?onload=onloadRecaptchaCallback&render=explicit"; queryParams += "&hl=" + runtimeLocale; scr.src = url + queryParams; diff --git a/ui.frontend/package-lock.json b/ui.frontend/package-lock.json index c786d7e3fc..4c6b2c51ce 100644 --- a/ui.frontend/package-lock.json +++ b/ui.frontend/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@aemforms/af-core": "^1.0.4", - "@aemforms/af-custom-functions": "1.0.17", + "@aemforms/af-custom-functions": "1.0.18", "@aemforms/af-formatters": "^1.0.4" }, "devDependencies": { @@ -117,9 +117,9 @@ } }, "node_modules/@aemforms/af-custom-functions": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@aemforms/af-custom-functions/-/af-custom-functions-1.0.17.tgz", - "integrity": "sha512-gDPC/Ly/+aQ7ojSPWm4JM3Zs+NUA+8c6bhjVbKm058AX5UEsXRk+N2o9wK4AHrdsaiR21vmvDuIUyUR2Jb2LTA==" + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/@aemforms/af-custom-functions/-/af-custom-functions-1.0.18.tgz", + "integrity": "sha512-Hg7dkONjmR3aH/Iec+/HRY/lCzVvP3GI6H9wD5FNGsE9zSxPcCzMtnAA0P7n/RKMjcnqYBMt4pFQ6MVb+YvIGg==" }, "node_modules/@aemforms/af-formatters": { "version": "1.0.4", @@ -11133,9 +11133,9 @@ } }, "@aemforms/af-custom-functions": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@aemforms/af-custom-functions/-/af-custom-functions-1.0.17.tgz", - "integrity": "sha512-gDPC/Ly/+aQ7ojSPWm4JM3Zs+NUA+8c6bhjVbKm058AX5UEsXRk+N2o9wK4AHrdsaiR21vmvDuIUyUR2Jb2LTA==" + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/@aemforms/af-custom-functions/-/af-custom-functions-1.0.18.tgz", + "integrity": "sha512-Hg7dkONjmR3aH/Iec+/HRY/lCzVvP3GI6H9wD5FNGsE9zSxPcCzMtnAA0P7n/RKMjcnqYBMt4pFQ6MVb+YvIGg==" }, "@aemforms/af-formatters": { "version": "1.0.4", diff --git a/ui.frontend/package.json b/ui.frontend/package.json index 501e5155d9..4402f857da 100644 --- a/ui.frontend/package.json +++ b/ui.frontend/package.json @@ -25,6 +25,6 @@ "dependencies": { "@aemforms/af-core": "^1.0.4", "@aemforms/af-formatters": "^1.0.4", - "@aemforms/af-custom-functions": "1.0.17" + "@aemforms/af-custom-functions": "1.0.18" } } diff --git a/ui.frontend/src/customFunctions.js b/ui.frontend/src/customFunctions.js index 64ae259d79..42cbab36b3 100644 --- a/ui.frontend/src/customFunctions.js +++ b/ui.frontend/src/customFunctions.js @@ -124,7 +124,7 @@ export const customFunctions = { /** * Fetches the captcha token for the form. * - * This function uses the Google reCAPTCHA Enterprise service to fetch the captcha token. + * Supports turnstile, reCAPTCHA Enterprise, and reCAPTCHA v3 via @aemforms/af-custom-functions. * * @async * @param {object} globals - An object containing read-only form instance, read-only target field instance and methods for form modifications. diff --git a/ui.tests/test-module/.gitignore b/ui.tests/test-module/.gitignore new file mode 100644 index 0000000000..2563da53f0 --- /dev/null +++ b/ui.tests/test-module/.gitignore @@ -0,0 +1,2 @@ +# Local Cypress secrets/overrides — must never be committed +cypress.env.json diff --git a/ui.tests/test-module/specs/recaptcha/recaptcha.authoring.cy.js b/ui.tests/test-module/specs/recaptcha/recaptcha.authoring.cy.js index 0d4422af2e..ebc55b75de 100644 --- a/ui.tests/test-module/specs/recaptcha/recaptcha.authoring.cy.js +++ b/ui.tests/test-module/specs/recaptcha/recaptcha.authoring.cy.js @@ -75,6 +75,12 @@ describe('Page - Authoring', function () { cy.get("input[name='./recaptchaSize'][value='normal']").should("be.disabled"); cy.get("input[name='./recaptchaSize'][value='compact']").should("be.disabled"); + // reCAPTCHA v3 is badge-only (no visible challenge), so size must be disabled like enterprise-score. + cy.get(".cmp-adaptiveform-recaptcha__configuration").click().then(() => { + cy.get("coral-selectlist-item[value='v3']").click(); + cy.get("input[name='./recaptchaSize'][value='normal']").should("be.disabled"); + cy.get("input[name='./recaptchaSize'][value='compact']").should("be.disabled"); + }) cy.get(".cmp-adaptiveform-recaptcha__configuration").click().then(() => { cy.get("coral-selectlist-item[value='v2checkbox']").click(); diff --git a/ui.tests/test-module/specs/recaptcha/recaptcha.runtime.cy.js b/ui.tests/test-module/specs/recaptcha/recaptcha.runtime.cy.js index 55aca9f7fc..ff0a1a40ea 100644 --- a/ui.tests/test-module/specs/recaptcha/recaptcha.runtime.cy.js +++ b/ui.tests/test-module/specs/recaptcha/recaptcha.runtime.cy.js @@ -21,12 +21,15 @@ describe("Form Runtime with Recaptcha Input", () => { const pagePath = "content/forms/af/core-components-it/samples/recaptcha/basic.html" const v2checkboxPagePath = "content/forms/af/core-components-it/samples/recaptcha/v2checkbox.html" const enterprisePagePath = "content/forms/af/core-components-it/samples/recaptcha/enterprisescore.html" + const v3PagePath = "content/forms/af/core-components-it/samples/recaptcha/v3.html" const bemBlock = 'cmp-adaptiveform-recaptcha' const IS = "adaptiveFormRecaptcha" const selectors = { recaptcha : `[data-cmp-is="${IS}"]` }; + const FT_RECAPTCHA_V3 = "FT_FORMS-27648"; + let formContainer = null let toggle_array = []; @@ -134,6 +137,18 @@ describe("Form Runtime with Recaptcha Input", () => { }) } + function updateRecaptchaV3Config(score) { + const secretKey = Cypress.env('RECAPTCHA_V3_API_KEY'); + cy.openPage("/mnt/overlay/fd/af/cloudservices/recaptcha/properties.html?item=%2Fconf%2Fcore-components-it%2Fsamples%2Frecaptcha%2Fbasic%2Fsettings%2Fcloudconfigs%2Frecaptcha%2Fv3").then(x => { + cy.get('#recaptcha-cloudconfiguration-secret-key').clear().type(secretKey); + // Target the inner native input of the coral-numberinput directly and force past the + // visibility check (the threshold container may not be shown, e.g. when the v3 toggle is + // off in the properties page) — the value still syncs to the field and is submitted on save. + cy.get('#recaptcha-cloudconfiguration-threshold-score input').clear({force: true}).type(score, {force: true}); + cy.get("#shell-propertiespage-doneactivator").click(); + }) + } + if (toggle_array.includes(FT_CLOUD_CONFIG_PROVIDER)) { it("client side validation should fail if recaptcha is not filled", () => { cy.previewForm(v2checkboxPagePath).then((p) => { @@ -253,4 +268,78 @@ describe("Form Runtime with Recaptcha Input", () => { } }) } + + it("should render reCAPTCHA v3 as an invisible badge", () => { + if (cy.af.isLatestAddon() && toggle_array.includes(FT_RECAPTCHA_V3)) { + cy.previewForm(v3PagePath).then((p) => { + formContainer = p; + }); + expect(formContainer, "formcontainer is initialized").to.not.be.null; + cy.wrap().then(() => { + const [id] = Object.entries(formContainer._fields).find(([id]) => id.includes("captcha")); + // v3 has no visible challenge; the widget must render badge-only (invisible). + cy.get(`#${id} .cmp-adaptiveform-recaptcha__widget > div.g-recaptcha`) + .should('exist') + .and('have.class', 'g-recaptcha-invisible'); + }); + } + }) + + it("submission should pass for reCAPTCHA v3", () => { + if (cy.af.isLatestAddon() && toggle_array.includes(FT_RECAPTCHA_V3)) { + // Exercises the v3 token fetch + submit flow. The secret key is injected via the + // RECAPTCHA_V3_API_KEY env var; the site key lives in the v3 cloud config fixture. + // Threshold of 0 so a token from a headless browser passes server-side verification. + updateRecaptchaV3Config(0); + cy.previewForm(v3PagePath).then((p) => { + formContainer = p; + }); + expect(formContainer, "formcontainer is initialized").to.not.be.null; + cy.get(`div.grecaptcha-badge`).should('exist').then(() => { + cy.intercept('POST', /\/adobe\/forms\/af\/submit\/.*/).as('submitForm'); + const submitForm = () => { + cy.get(`.cmp-adaptiveform-button__widget`).click(); + return cy.wait('@submitForm', { timeout: 50000 }).then((interception) => { + if (interception.response.statusCode === 200) { + cy.log('Submit request succeeded'); + return cy.wrap(true); + } else { + cy.log('Submit request failed, retrying...'); + return cy.wrap(false); + } + }); + }; + // Retry like the enterprise-score test, since reCAPTCHA can intermittently return a + // browser-error on the first assessment. + cy.waitUntil(() => submitForm(), { + errorMsg: 'Maximum retry limit reached, request did not succeed', + timeout: 50000, + interval: 5000, + }); + }); + } + }) + + it("submission should fail for reCAPTCHA v3", () => { + if (cy.af.isLatestAddon() && toggle_array.includes(FT_RECAPTCHA_V3)) { + // Threshold of 1.0 is above any real headless-browser score, so server-side + // verification always rejects the token — deterministically exercises the failure path. + updateRecaptchaV3Config(1.0); + cy.on('window:alert', (message) => { + expect(message).to.equal('Encountered an internal error while submitting the form.'); + }); + cy.intercept('POST', /\/adobe\/forms\/af\/submit\/.*/).as('submitForm'); + cy.previewForm(v3PagePath).then((p) => { + formContainer = p; + }); + expect(formContainer, "formcontainer is initialized").to.not.be.null; + cy.get(`div.grecaptcha-badge`).should('exist').then(() => { + cy.get(`.cmp-adaptiveform-button__widget`).click(); + cy.wait('@submitForm', { timeout: 50000 }).then((interception) => { + expect(interception.response.statusCode).to.equal(400); + expect(interception.response.body).to.have.property('title', 'The CAPTCHA validation failed. Please try again.'); + }); + }); + } + }) })